Sharing Angular 5 Modules
If you’ve ever worked on a project where you’ve tried to share code between two (or more) projects, you probably understand the pain. Sadly, I’ve worked on a few.
Admittedly, on the surface it seems like a tempting proposition. On paper, it sounds quite easy. For example, one system I worked on we had a single nuget library where we kept our domain objects. The logic went: “All you have to do when you want to make a change is update the domain object project, build, push the new library to the nuget server, then pull down the new library to the other projects, and then rebuild those. It’s easy.”
It’s a disaster.
You quickly start running into versioning problems, build timing problems, people updating things without letting other people know - not to mention the amount of time to do something very simple start to take forever because all of the steps - it just gets messy really fast.
It’s popular in Golang, for example, to pull down all the source code you need for a project, save that and build from there. That works well, unless you’re trying to share interfaces for a rest service that is in active development and the contract is changing frequently. It winds up being the same problem, you just git pull
instead of update
.
I am working for the New Zealand based company PartPay, and we have started running into this problem in our Angular code. We now have three Angular 5 projects that are using some similar code. Some interfaces for services, some angular 5 services, and some customized logic that would be really nice to have in one place.
Over the last week we refactored some bits and found, what I think, is a very nice solution. We didn’t so much find it, as just implement what Angular 5 Cli just does. However, I haven’t seen anyone really talk about how to do this.
The executive summary is: we put all the project’s code in one project, referenced bits between projects, and let webpack build the separate apps.
Do The Project Layout Backwards
Initially, we put all the Angular code in one repository, but in separate Angular projects like this:
Common/
src/
app/
Project1/
src/
app/
Project2/
src/
app/
Project3/
src/
app/
And we then built everything at once. That made sure everything was always building with the same interfaces and services, and it would error if a common change broke something in one of the projects.
So we tried just referencing different projects from within other projects. Something like this:
// in Project 1
import { StuffService } form '../../../Common/src/app/module/stuff.service'
Logically this should work. It even seems to work as it “compiles” and webpack seemingly has no problems bundling the code. The problem with this is at runtime. For some reason, NgZone just freaks out. My guess is that with other frameworks (or your own framework) this would work just fine - but Angular can’t handle this setup.
The fix is to do it backwards. Instead of multiple Angular projects, you make one Angular project with several apps. You do the layout this way:
SuperProject/
src/
app-common/
app-project1/
app-project2/
app-project3/
And then share via:
// in Project 1
import { StuffService } form '../../app-common/module/stuff.service'
this even seems to work:
// in Project 1
import { StuffService } form 'app-common/module/stuff.service'
This is really cool as you now only need one npm install (or yarn install) for all the projects which saves times on the build server.
I should also mention that this requires you to be using the angular-cli. If you eject the webpack config, it doesn’t work out of the box. You need to be using ng serve and ng build for this to Just Workβ’.
Update Angular-cli.json
When you go to build the projects, you’ll want them to go into different directories and also to likely have different index.html pages. To sort this out, you’ll need to update the .angular-cli.json file. Here is an example of ours:
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
"name": "part-pay.common.website"
},
"apps": [
{
"name": "app-common",
"root": "src",
"outDir": "dist/common",
"assets": [
"assets",
"settings.json",
"favicon.ico"
],
"index": "index-common.html",
"main": "main-common.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "pp",
"styles": [],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
},
{
"name": "app-project1",
"root": "src",
"outDir": "dist/project1",
"assets": [
"assets",
"settings.json",
"web.config"
],
"index": "index-project1.html",
"main": "main-project1.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "pp",
"styles": [
"styles/partpay-theme.scss",
"styles/merchant/styles.scss"
],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
},
Notice the different sections of the app array, and also notice that we’re using different index-[something].html and main-[something].ts for entry files. We have an extra scripting layer when we deploy that moves and renames files as needed (like rename to index.html).
Update Package.json
Like angular-cli.json you’ll need to mess a bit with the package.json. Here is an example of ours:
...
"test": "ng test --browsers ChromeHeadless --single-run",
"lint": "ng lint",
...
"start:checkout": "ng serve --app app-project1 --host 0.0.0.0 --port 9001",
"start:customer": "ng serve --app app-project2 --host 0.0.0.0 --port 9002",
"start:merchant": "ng serve --app app-project3 --host 0.0.0.0 --port 9003",
"start:common": "ng serve --app app-common --host 0.0.0.0 --port 9010",
"build:checkout": "ng build --app app-project1 --prod --aot true -t production -e prod --vendor-chunk=true",
"build:customer": "ng build --app app-project2 --prod --aot true -t production -e prod --vendor-chunk=trueβ,
"build:merchant": "ng build --app app-project3 --prod --aot true -t production -e prod --vendor-chunk=true",
Aside from just needing the one yarn install, you also just need one test run too. I really like this because it gives us confidence that when there is a change to common, all the projects that are currently using that code also run their tests. Noice.
Output
If you configure your .angular-cli.json similarly to the above example, doing a build will give you something like the following (in your dist directory):
Robs-MacBook-Pro:Common robrohan$ tree -L 2 dist
dist
βββ project3
β βββ 3rdpartylicenses.txt
β βββ assets
β βββ index-project1.html
β βββ inline.e6d173c400ee8368d31d.bundle.js
β βββ main.d5aa665aaf038618d003.bundle.js
β βββ polyfills.30f325d8f0721457d5e0.bundle.js
β βββ styles.1bee0de3213989a458f2.bundle.css
β βββ vendor.1b61d1024c384094bb6b.bundle.js
β βββ web.config
βββ project3
β βββ 3rdpartylicenses.txt
β βββ assets
β βββ index-project2.html
β βββ inline.72852e24a300baea5af4.bundle.js
β βββ main.d14c37882bf3210788bc.bundle.js
β βββ polyfills.30f325d8f0721457d5e0.bundle.js
β βββ styles.7696b644715a9f256722.bundle.css
β βββ vendor.ba82eb312e9aab2cc8b5.bundle.js
β βββ web.config
βββ project3
βββ 3rdpartylicenses.txt
βββ assets
βββ index-project3.html
βββ inline.88740e7a4e3716c1efcc.bundle.js
βββ main.db8e228549fbe96675f9.bundle.js
βββ polyfills.30f325d8f0721457d5e0.bundle.js
βββ styles.914bbb5bdb522acc71dc.bundle.css
βββ vendor.090fe093e78811aa6f93.bundle.js
βββ web.config
From here it’s just a matter of deploying.
Conculsion
I quite like this setup. It’s not only helped us remove some duplicate / similar code, it has also increased our confidence around testing as well as made our build time shorter. Here is a quick pro / con list:
Pros
- Reuse of services / domain objects / interfaces / css / images
- One environment file, lint config etc
- Only one yarn / npm install (faster build time)
- Testing across all projects at once
Cons
- Canβt eject webpack.config - so no custom webpack plugins :(
- Need to move index files when building
Reduce, Reuse, Recycle, Refactor!