Migrating an AngularJS Project into an Nx Workspace
Nx offers first-class support for Angular and React out-of-the-box. But one of the questions the Nrwl team often hears from our community is how to use AngularJS (Angular 1.x) in Nx. Nx is a great choice for managing an AngularJS to Angular upgrade, or just for consolidating your existing polyrepo approach to AngularJS into a monorepo to make maintenance a little easier.
In this article, you’ll learn how to:
- Create an Nx workspace for an AngularJS application
- Migrate an AngularJS application into your Nx workspace
- Convert an existing build process for use in Nx
- Use Webpack to build an AngularJS application
- Run unit and end-to-end tests
For this example, you’ll be migrating the Real World AngularJS application from Thinkster.io. You should clone this repo so you have access to the code before beginning.
There is also a repo that shows a completed example of this guide.
The RealWorld app is a great example of an AngularJS app, but it probably doesn’t have the complexity of your own codebase. As you go along, I’ll include some recommendations on how you might apply this example to your larger, more complex application.
Creating your workspace
To start migrating the Real World app, create an Nx workspace:
npx create-nx-workspace@latest nx-migrate-angularjs
When prompted choose the empty
preset. The other presets use certain recommended defaults for the workspace configuration. Because you have existing code with specific requirements for configuration, starting with a blank workspace avoids resetting these defaults. This will give you the ability to customize the workspace for the incoming code.
At the next prompt, choose Angular CLI
for your workspace CLI. While you may not be using Angular now, this gives you the best option to upgrade to Angular later. The Angular CLI is also the best CLI option for using Karma and Protractor, the two testing suites most commonly used for AngularJS.
? What to create in the new workspace empty [an empty workspace]
? CLI to power the Nx workspace Angular CLI [Extensible CLI for Angular applications. Recommended for Angular projects.]
Creating your app
Your new workspace won’t have much in it because of the empty
preset. You’ll need to generate an application to have some structure created. Add the Angular capability to your workspace:
ng add @nrwl/angular
When prompted, make a choice of unit test runner and e2e test runner:
? Which Unit Test Runner would you like to use for the application? Karma [ https://karma-runner.github.io ]
? Which E2E Test Runner would you like to use? Protractor [ https://www.protractortest.org ]
For this example, we will use Karma and Protractor, the most common unit test runner and e2e test runner for AngularJS.
Codebases with existing unit and e2e tests should continue to use whatever runner they need. We’ve chosen Karma and Protractor here because it’s the most common. If you’re going to be adding unit testing or e2e as part of this transition and are starting fresh, we recommend starting with Jest and Cypress.
With the Angular capability added, generate your application:
ng generate @nrwl/angular:application --name=realworld
Accept the default options for each prompt:
? Which stylesheet format would you like to use? CSS
? Would you like to configure routing for this application? No
The RealWorld app doesn’t have any styles to actually bundle here. They’re all downloaded from a CDN that all of the RealWorld apps use. If your codebase uses something other than CSS, like Sass or PostCSS, you can choose that here.
Migrating dependencies
Copy the dependencies from the RealWorld app’s package.json
to the package.json
in your workspace. Split the existing dependencies into dependencies
(application libraries) and devDependencies
(build and test libraries). Everything related to gulp can go into devDependencies
.
Your package.json
should now look like this:
1{
2 "name": "nx-migrate-angularjs",
3 "version": "0.0.0",
4 "license": "MIT",
5 "scripts": {
6 "ng": "ng",
7 "nx": "nx",
8 "start": "ng serve",
9 "build": "ng build",
10 "test": "ng test",
11 "lint": "nx workspace-lint && ng lint",
12 "e2e": "ng e2e",
13 "affected:apps": "nx affected:apps",
14 "affected:libs": "nx affected:libs",
15 "affected:build": "nx affected:build",
16 "affected:e2e": "nx affected:e2e",
17 "affected:test": "nx affected:test",
18 "affected:lint": "nx affected:lint",
19 "affected:dep-graph": "nx affected:dep-graph",
20 "affected": "nx affected",
21 "format": "nx format:write",
22 "format:write": "nx format:write",
23 "format:check": "nx format:check",
24 "update": "ng update @nrwl/workspace",
25 "workspace-generator": "nx workspace-generator",
26 "dep-graph": "nx dep-graph",
27 "help": "nx help",
28 "postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points"
29 },
30 "private": true,
31 "dependencies": {
32 "@nrwl/angular": "^9.0.4",
33 "@angular/animations": "9.0.0",
34 "@angular/common": "9.0.0",
35 "@angular/compiler": "9.0.0",
36 "@angular/core": "9.0.0",
37 "@angular/forms": "9.0.0",
38 "@angular/platform-browser": "9.0.0",
39 "@angular/platform-browser-dynamic": "9.0.0",
40 "@angular/router": "9.0.0",
41 "angular": "^1.5.0-rc.2",
42 "angular-ui-router": "^0.4.2",
43 "core-js": "^2.5.4",
44 "rxjs": "~6.5.0",
45 "zone.js": "^0.10.2"
46 },
47 "devDependencies": {
48 "@angular/cli": "9.0.1",
49 "@nrwl/workspace": "9.0.4",
50 "@types/node": "~8.9.4",
51 "dotenv": "6.2.0",
52 "ts-node": "~7.0.0",
53 "tslint": "~5.11.0",
54 "eslint": "6.1.0",
55 "typescript": "~3.7.4",
56 "prettier": "1.18.2",
57 "@angular/compiler-cli": "9.0.0",
58 "@angular/language-service": "9.0.0",
59 "@angular-devkit/build-angular": "0.900.1",
60 "codelyzer": "~5.0.1",
61 "karma": "~4.0.0",
62 "karma-chrome-launcher": "~2.2.0",
63 "karma-coverage-istanbul-reporter": "~2.0.1",
64 "karma-jasmine": "~1.1.2",
65 "karma-jasmine-html-reporter": "^0.2.2",
66 "jasmine-core": "~2.99.1",
67 "jasmine-spec-reporter": "~4.2.1",
68 "@types/jasmine": "~2.8.8",
69 "protractor": "~5.4.0",
70 "@types/jasminewd2": "~2.0.3",
71 "babel-preset-es2015": "^6.3.13",
72 "babelify": "^7.2.0",
73 "browser-sync": "^2.11.1",
74 "browserify": "^13.0.0",
75 "browserify-ngannotate": "^2.0.0",
76 "gulp": "^3.9.1",
77 "gulp-angular-templatecache": "^1.8.0",
78 "gulp-notify": "^2.2.0",
79 "gulp-rename": "^1.2.2",
80 "gulp-uglify": "^1.5.3",
81 "gulp-util": "^3.0.7",
82 "marked": "^0.3.5",
83 "merge-stream": "^1.0.0",
84 "vinyl-source-stream": "^1.1.0"
85 }
86}
Run npm install
to install all of your new dependencies.
For your own project, you’ll need to switch to NPM if you’re using another package manager like bower. Learn more about switching away from bower
Migrating application code
This Angular application that you generated has the configuration that you need, but you don’t need any of its application code. You’ll replace that with the RealWorld app code. Delete the contents of apps/realworld/src/app
.
Starting in the js
folder of the realworld app, copy all of the application code into apps/realworld/src/app
. The resulting file tree should look like this:
1apps
2|____realworld-e2e
3|____realworld
4| |____src
5| | |____index.html
6| | |____app
7| | | |____settings
8| | | |____home
9| | | |____config
10| | | |____auth
11| | | |____layout
12| | | |____components
13| | | |____profile
14| | | |____article
15| | | |____services
16| | | |____editor
17| | | |____app.js\
18| | |____styles.css
19| | |____environments
20| | |____main.ts
21| | |____test.ts
22| | |____assets
You most likely have your own AngularJS project written in JavaScript as well. While you’ll continue to use JavaScript through the rest of this example, we strongly recommend switching AngularJS projects to TypeScript, especially if you’re planning an upgrade to Angular.
Modifying index.html and main.ts
Your generated application will also have an index.html
provided. However, it’s set up for an Angular application, not an AngularJS application. Replace the contents of apps/realworld/src/index.html
with the index.html
from the RealWorld app.
Your application also has a main.ts
file which is responsible for bootstrapping your app. Again, you don’t need much from this file any more. Replace its contents with:
1import ‘./app/app.js’;
And re-name it to main.js
. This will import the existing app.js file from the RealWorld app which will bootstrap the app.
Adding existing build and serve processes
If you’re looking at the example repo, the code for this section is available on branch initial-migration
. This section is an interim step that continues to use gulp to build and serve the app locally. You’ll replace gulp in the next section. The RealWorld app uses gulp 3.9.1 to build. This version is not supported anymore and doesn’t run on any version of Node greater than 10.*. To build this using gulp, you need to install an appropriate version of Node and make sure you re-install your dependencies. If this isn’t possible (or you just don’t want to), feel free to skip to the next section. The webpack build process should run in any modern Node version.
The RealWorld app uses gulp to build the application, as well as provide a development server. To verify that the migration has worked, stay with that build process for now.
During migration, you should take a small step and confirm that things work before moving ahead. Stopping and checking to see that your app still builds and functions is essential to a successful migration.
Copy the gulpfile.js
over from the RealWorld app and put it in apps/realworld
. This is where all configuration files reside for the application. Make some adjustments to this file so that your build artifacts land in a different place. In an Nx workspace, all build artifacts should be sent to an app-specific folder in the dist
folder at the root of your workspace. Your gulpfile.js
should look like this:
1var gulp = require('gulp');
2var notify = require('gulp-notify');
3var source = require('vinyl-source-stream');
4var browserify = require('browserify');
5var babelify = require('babelify');
6var ngAnnotate = require('browserify-ngannotate');
7var browserSync = require('browser-sync').create();
8var rename = require('gulp-rename');
9var templateCache = require('gulp-angular-templatecache');
10var uglify = require('gulp-uglify');
11var merge = require('merge-stream');
12
13// Where our files are located
14var jsFiles = 'src/app/**/*.js';
15var viewFiles = 'src/app/**/*.html';
16
17var interceptErrors = function (error) {
18 var args = Array.prototype.slice.call(arguments);
19
20 // Send error to notification center with gulp-notify
21 notify
22 .onError({
23 title: 'Compile Error',
24 message: '<%= error.message %>',
25 })
26 .apply(this, args);
27
28 // Keep gulp from hanging on this task
29 this.emit('end');
30};
31
32gulp.task('browserify', ['views'], function () {
33 return (
34 browserify('./src/main.js')
35 .transform(babelify, { presets: ['es2015'] })
36 .transform(ngAnnotate)
37 .bundle()
38 .on('error', interceptErrors)
39 //Pass desired output filename to vinyl-source-stream
40 .pipe(source('main.js'))
41 // Start piping stream to tasks!
42 .pipe(gulp.dest('../../dist/apps/realworld/'))
43 );
44});
45
46gulp.task('html', function () {
47 return gulp
48 .src('src/index.html')
49 .on('error', interceptErrors)
50 .pipe(gulp.dest('../../dist/apps/realworld/'));
51});
52
53gulp.task('views', function () {
54 return gulp
55 .src(viewFiles)
56 .pipe(
57 templateCache({
58 standalone: true,
59 })
60 )
61 .on('error', interceptErrors)
62 .pipe(rename('app.templates.js'))
63 .pipe(gulp.dest('src/app/config'));
64});
65
66// This task is used for building production ready
67// minified JS/CSS files into the dist/ folder
68gulp.task('build', ['html', 'browserify'], function () {
69 var html = gulp
70 .src('../../dist/apps/realworld/index.html')
71 .pipe(gulp.dest('../../dist/apps/realworld/'));
72
73 var js = gulp
74 .src('../../dist/apps/realworld/main.js')
75 .pipe(uglify())
76 .pipe(gulp.dest('../../dist/apps/realworld/'));
77
78 return merge(html, js);
79});
80
81gulp.task('default', ['html', 'browserify'], function () {
82 browserSync.init(['../../dist/apps/realworld/**/**.**'], {
83 server: '../../dist/apps/realworld',
84 port: 4000,
85 notify: false,
86 ui: {
87 port: 4001,
88 },
89 });
90
91 gulp.watch('src/index.html', ['html']);
92 gulp.watch(viewFiles, ['views']);
93 gulp.watch(jsFiles, ['browserify']);
94});
You need to point your build
and serve
tasks at this gulp build process. Typically, an Angular app is built using the Angular CLI, but the Angular CLI doesn’t know how to build AngularJS projects. All of your tasks are configured in the angular.json
file. Find the build
and serve
tasks and replace them with this code block:
1...
2 "build": {
3 "builder": "@nrwl/workspace:run-commands",
4 "options": {
5 "commands": [
6 {
7 "command": "npx gulp --gulpfile apps/realworld/gulpfile.js build"
8 }
9 ]
10 }
11 },
12 "serve": {
13 "builder": "@nrwl/workspace:run-commands",
14 "options": {
15 "commands": [
16 {
17 "command": "npx gulp --gulpfile apps/realworld/gulpfile.js"
18 }
19 ]
20 }
21 },
22...
This sets up the build
and serve
commands to use the locally installed version of gulp to run build
and serve
. To see the RealWorld app working, run
ng serve realworld
Navigate around the application and see that things work.
Your own project might not be using gulp. If you’re using webpack, you can follow the next section and substitute your own webpack configuration. If you’re using something else like grunt or a home-grown solution, you can follow the same steps here to use it. You’ll use the
run-commands
builder and substitute in the commands for your project.
Switching to webpack
So far, you’ve mostly gotten already existing code and processes to work. This is the best way to get started with any migration: get existing code to work before you start making changes. This gives you a good, stable base to build on. It also means you having working code now rather than hoping you’ll have working code in the future!
But migrating AngularJS code means we need to switch some of our tools to a more modern tool stack. Specifically, using webpack and babel is going to allow us to take advantage of Nx more easily. Becoming an expert in these build tools is outside the scope of this article, but I’ll address some AngularJS specific concerns. To get started, install a new dependency:
npm install babel-plugin-angularjs-annotate
Nx already has most of what you need for webpack added as a dependency. babel-plugin-angularjs-annotate
is going to accomplish the same thing that browserify-ngannotate
previously did in gulp: add dependency injection annotations.
Start with a webpack.config.js
file in your application’s root directory:
1const path = require('path');
2
3module.exports = (config, context) => {
4 return {
5 ...config,
6 module: {
7 strictExportPresence: true,
8 rules: [
9 {
10 test: /\.html$/,
11 use: [{ loader: 'raw-loader' }],
12 },
13 // Load js files through Babel
14 {
15 test: /\.(js|jsx)$/,
16 loader: 'babel-loader',
17 options: {
18 presets: ['@babel/preset-env'],
19 plugins: ['angularjs-annotate'],
20 },
21 },
22 ],
23 },
24 };
25};
This webpack configuration is deliberately simplified for this tutorial. A real production-ready webpack config for your project will be much more involved. See this project for an example.
To use webpack instead of gulp, go back to your angular.json
file and modify the build
and serve
commands again:
1...
2"build": {
3 "builder": "@nrwl/web:build",
4 "options": {
5 "outputPath": "dist/apps/realworld",
6 "index": "apps/realworld/src/index.html",
7 "main": "apps/realworld/src/main.ts",
8 "polyfills": "apps/realworld/src/polyfills.ts",
9 "tsConfig": "apps/realworld/tsconfig.app.json",
10 "assets": [
11 "apps/realworld/src/favicon.ico",
12 "apps/realworld/src/assets"
13 ],
14 "styles": ["apps/realworld/src/styles.css"],
15 "scripts": [],
16 "webpackConfig": "apps/realworld/webpack.config",
17 "buildLibsFromSource": true
18 },
19 "configurations": {
20 "production": {
21 "fileReplacements": [
22 {
23 "replace": "apps/realworld/src/environments/environment.ts",
24 "with": "apps/realworld/src/environments/environment.prod.ts"
25 }
26 ],
27 "optimization": true,
28 "outputHashing": "all",
29 "sourceMap": false,
30 "extractCss": true,
31 "namedChunks": false,
32 "extractLicenses": true,
33 "vendorChunk": false,
34 "budgets": [
35 {
36 "type": "initial",
37 "maximumWarning": "2mb",
38 "maximumError": "5mb"
39 }
40 ]
41 }
42 }
43},
44"serve": {
45 "builder": "@nrwl/web:dev-server",
46 "options": {
47 "buildTarget": "realworld:build"
48 }
49},
50...
You may have noticed a rule for loading HTML in webpack.config.js
. You need to modify some of your AngularJS code to load HTML differently. The application previously used the template cache to store all of the component templates in code, rather than download them at run time. This works, but you can do things a little better with webpack.
Rather than assigning templateUrl
for your components, you can instead import the HTML and assign it to the template
attribute. This is effectively the same as writing your templates in-line, but you still have the benefit of having a separate HTML file. The advantage is that the template is tied to its component, not a global module like the template cache. Loading all templates into the template cache is more performant than individually downloading templates, but it also means your user is downloading every single component’s template as part of start-up. This was fine in AngularJS when you didn’t easily have access to lazy-loading, so you always had a large up-front download cost. As you begin to upgrade to Angular or other modern frontend frameworks, you will gain access to lazy-loading: only loading code when it’s necessary. By making this change now, you set yourself up for success later.
To accomplish this, open config/app.config.js
which is the main app component. Modify it like this:
1import authInterceptor from './auth.interceptor';
2import template from '../layout/app-view.html';
3
4function AppConfig(
5 $httpProvider,
6 $stateProvider,
7 $locationProvider,
8 $urlRouterProvider
9) {
10 'ngInject';
11
12 $httpProvider.interceptors.push(authInterceptor);
13
14 /*
15 If you don't want hashbang routing, uncomment this line.
16 Our tutorial will be using hashbang routing though :)
17 */
18 // $locationProvider.html5Mode(true);
19
20 $stateProvider.state('app', {
21 abstract: true,
22 template,
23 resolve: {
24 auth: function (User) {
25 return User.verifyAuth();
26 },
27 },
28 });
29
30 $urlRouterProvider.otherwise('/');
31}
32
33export default AppConfig;
This change loads the HTML code directly and sets it to the template attribute of the component. The HTML rule that you specified in the webpack config will take care of loading the HTML correctly and adding it to the template like this.
Now, go through each component of the application and make this change. To make sure that you’ve really modified every component correctly, delete the template cache file (config/app.templates.js
) that gulp generated earlier.
In an example like this, it’s easy enough to make this kind of change by hand. In a larger codebase, doing this manually could be very time-intensive. You’ll want to look into an automated tool to do this for you, such as js-codemod or generators.
Run the application the same way as before:
ng serve realworld
Unit testing
Unit testing can be an important part of any code migration. If you migrate your code into a new system, and all of your unit tests pass, you have a higher degree of confidence that your application actually works without manually testing. Unfortunately, the RealWorld application doesn’t have any unit tests, but you can add your own.
You need a few dependencies for AngularJS unit testing that Nx doesn’t provide by default:
npm install angular-mocks@1.5.11 karma-webpack
Earlier, you configured this app to use Karma as its unit test runner. Nx has provided a Karma config file for you, but you’ll need to modify it to work with AngularJS:
1const webpack = require('./webpack.config');
2const getBaseKarmaConfig = require('../../karma.conf');
3
4module.exports = function (config) {
5 const baseConfig = getBaseKarmaConfig();
6 config.set({
7 ...baseConfig,
8 frameworks: ['jasmine'],
9 plugins: [
10 require('karma-jasmine'),
11 require('karma-chrome-launcher'),
12 require('karma-jasmine-html-reporter'),
13 require('karma-coverage-istanbul-reporter'),
14 require('karma-webpack'),
15 ],
16 // This will be the new entry to webpack
17 // so it should just be a single file
18 files: ['src/test.js'],
19
20 // Preprocess test index and test files using
21 // webpack (will run babel)
22 preprocessors: {
23 'src/test.js': ['webpack'],
24 'src/**/*.spec.js': ['webpack'],
25 },
26
27 // Reference webpack config (single object)
28 // and configure some middleware settings
29 webpack: {
30 ...webpack({}),
31 mode: 'development',
32 },
33 webpackMiddleware: {
34 noInfo: true,
35 stats: 'errors-only',
36 },
37
38 // Typical Karma settings, see docs
39 reporters: ['progress'],
40 port: 9876,
41 colors: true,
42 logLevel: config.LOG_INFO,
43 autoWatch: true,
44 browsers: ['ChromeHeadless'],
45 singleRun: true,
46 concurrency: Infinity,
47 });
48};
Now add a unit test for the comment component:
1import articleModule from './index';
2
3beforeEach(() => {
4 // Create the module where our functionality can attach to
5 angular.mock.module('ui.router');
6 angular.mock.module(articleModule.name);
7});
8
9let component;
10
11beforeEach(
12 angular.mock.inject(($rootScope, $componentController) => {
13 let User = {
14 current: false,
15 };
16 component = $componentController('comment', { User });
17 })
18);
19
20describe('comment component', () => {
21 it('should be defined', () => {
22 expect(component).toBeDefined();
23 });
24
25 it('should default canModify to false', () => {
26 expect(component.canModify).toEqual(false);
27 });
28});
This unit test does a check to make sure the component compiles and that it sets default permissions correctly.
To run the unit tests:
ng test
You should see green text as your test passes.
End to End testing
End to End (or E2E) testing is another important part of migration. The more tests you have to verify your code, the easier it is to confirm that your code works the same way it did before. Again, the realworld application doesn’t have any e2e tests, so you need to add your own.
Nx created realworld-e2e
for you when you generated your app. There is an example test already generated in apps/realworld-e2e/src/app.e2e-spec.ts
. It has a helper file named app.po.ts
. The spec
file contains the actual tests, while the po
file contains helper functions to retrieve information about the page. The generated test checks to make sure the title of the app is displayed properly, an indication that the app bootstrapped properly in the browser.
You need to modify these files to account for the RealWorld app layout. Make the following modifications:
1//app.e2e-spec.ts
2import { AppPage } from './app.po';
3import { browser, logging } from 'protractor';
4
5describe('workspace-project App', () => {
6 let page: AppPage;
7
8 beforeEach(() => {
9 page = new AppPage();
10 });
11
12 it('should display app title', () => {
13 page.navigateTo();
14 expect(page.getTitleText()).toEqual('conduit');
15 });
16
17 afterEach(async () => {
18 // Assert that there are no errors emitted from the browser
19 const logs = await browser.manage().logs().get(logging.Type.BROWSER);
20 expect(logs).not.toContain(
21 jasmine.objectContaining({
22 level: logging.Level.SEVERE,
23 } as logging.Entry)
24 );
25 });
26});
1// app.po.ts
2import { browser, by, element } from 'protractor';
3
4export class AppPage {
5 navigateTo(): Promise<unknown> {
6 return browser.get(browser.baseUrl) as Promise<unknown>;
7 }
8
9 getTitleText(): Promise<string> {
10 return element(by.css('h1.logo-font')).getText() as Promise<string>;
11 }
12}
To run e2e tests, use the e2e
command:
ng e2e
You should see a browser pop up to run the Protractor tests and then green success text in your console.
Summary
- Nx workspaces can be customized to support AngularJS projects
- AngularJS projects can be migrated into an Nx workspace using existing build and serve processes
- Switching to Webpack can enable your Angular upgrade success later
- Unit testing and e2e testing can be used on AngularJS projects and can help ensure a successful migration