In our case, the app source and build folders will still be within the respective projects, but now contained within a parent Angular workspace and npm package...
- Faster, easier updates
- Less redundant code/architecture
- Increased developer productivity (by spending less time fighting the architecture and overhead)
- Code reuse.
One of our big goals is to get our 21 separate single-app Angular workspaces all to a single Angular workspace with many apps and projects. Currently, shamefully, we are at version 9. We want to enable trivial fast updates, and then keep the apps updated w/ a few mins of maintenance between each sprint moving forward.
Here's what we're starting with... I've swapped our app names with an equivalent structure using fictional Spotify entities as an example...
Spotify/
├── Tracks/
│ ├── angular.json
│ ├── package.json
│ ├── package-lock.json
│ ├── node_modules/
│ │ └── [packages]
│ ├── Controllers/
│ │ └── [server src]
│ ├── dist/
│ │ └── [angular build]
│ └── src/
│ └── [angular src]
├── Playlists/
│ ├── angular.json
│ ├── package.json
│ ├── package-lock.json
│ ├── node_modules/
│ │ └── [packages]
│ ├── Controllers/
│ │ └── [server src]
│ ├── dist/
│ │ └── [angular build]
│ └── src/
│ └── [angular src]
└── Artists/
├── angular.json
├── package.json
├── package-lock.json
├── node_modules/
│ └── [packages]
├── Controllers/
│ └── [server src]
├── dist/
│ └── [angular build]
└── src/
└── [angular src]
Yikes! so many duplicated files, dependencies, etc!
Each micro-frontend (Tracks, Playlists, Artists) is deployed to its own subdomain, so something like tracks.spotify.com, playlists.spotify.com, artists.spotify.com, etc.
To begin the migration, create a new Angular workspace in the closest shared parent folder. This new workspace Angular version should (attempt to) match the Angular version of the existing apps.
cd /path/to/Spotify
# use your preferred angular version;
# the ^ says use the latest 9.x
npx @angular/cli@^9.0.0 new \
--directory ./ \
--create-application false
The ng new command will create the following files within the existing directory structure...
Spotify/
├── angular.json
├── package.json
├── package-lock.json
└── node_modules/
└── [packages]
Now, migrate each app's package.json dependencies
and devDependencies
into the new parent package.json as well as any package scripts that should be migrated. If there are conflicts between library versions, you should resolve them one app migration at a time. At each app migration step this process, try to get your apps and its dependencies upgraded to the highest possible versions supported at the ng/node version levels.
When you're done, rename the {app}/package.json file (or delete it).
Next, open the {app}/angular.json
and locate the projects you want to keep. So for tracks, the project could be named 'tracks' and its test project would be 'tracks-test' and e2e would be 'tracks-e2e'. If you want to rename these to get everything consistent and tidy, you can.
Copy these config objects to the Spotify/angular.json projects
object, and update the paths found within the config. You can usually do this w/ a find and replace. For example, find 'src/app' and replace with 'Tracks/src/app'. (I used jq
and sed
in bash scripts to extract and fix these automatically)
Build the app and fix any issues. If everything is working as expected, you can clean things up by deleting each {app}/node_modules
, {app}/package.json
, {app}/package-lock.json
and {app}/angular.json
.
Optional, but recommended: add scripts to the parent package.json for build/lint/serve/clean...
{
"scripts": {
"tracks:clean": "rm -rf Tracks/dist",
"tracks:build": "ng build --project=tracks",
"tracks:serve": "ng serve --project=tracks",
"tracks:lint": "..."
}
}
Now is a good time to do a per-app git commit: git commit -m "Migrated Tracks app to unified Angular workspace"
(I also automated this using jq
and bash)
Spotify/
├── angular.json
├── package.json
├── package-lock.json
├── node_modules/
│ └── [shared packages]
├── Tracks/
│ ├── Controllers/
│ │ └── [server src]
│ ├── dist/
│ │ └── [angular build]
│ └── src/
│ └── [angular src]
├── Playlists/
│ ├── Controllers/
│ │ └── [server src]
│ ├── dist/
│ │ └── [angular build]
│ └── src/
│ └── [angular src]
└── Artists/
├── Controllers/
│ └── [server src]
├── dist/
│ └── [angular build]
└── src/
└── [angular src]
This is already looking tidier. Rinse and repeat until all apps are migrated and building.
Any future development work that includes updating a shared dependency in the package.json to accommodate/enable a change in one application will affect all applications! CI/CD build servers should detect a change to package.json and package-lock.json and trigger a build (and hopefully lint and test) for all apps. This endeavor is about increasing stability by decreasing overhead.
Now, you can use the ng generate
command to add new applications to the project. Let's say we are going to double down on this god-forsaken architecture and add a new micro-frontend for Lyrics...
Create the Spotify/Lyrics
folder/project for whatever your server-side env (node/.net/etc) is...
# from Spotify/ directory
npx ng generate application lyrics --project-root ./Lyrics/src
Since these are now all part of the same workspace, they can share code (services/components/guards/etc) with each other! You can even create a library
project of strictly library code...
# from Spotify/ directory
npx ng generate library shared --project-root ./Shared
You can now develop any of these apps using the ng serve
command, but passing a project name (the project key in the angular.json file)...
# from Spotify/ directory
npx ng serve --project=tracks
Or, if your server needs to serve the files for whatever reason, just run build watch with your server running (assuming it has some file that is generating a refreshable HTML file referencing the Angular artifacts in Spotify/{app}/dist
...
# from Spotify/ directory
npx ng build --watch --project=tracks
You can now upgrade the entire workspace to the next version. In our case, I would execute the following in an Angular 9.x workspace...
# from Spotify/ directory
npx ng update @angular/cli@^10.0.0 @angular/core@^10.0.0
You can also do neat things like adding ng-universal (server-side rendering) to apps! This is appealing since using express means you can use the same Interfaces and Classes in server-side code, but that's a topic for another day.
# from Spotify/ directory
ng add @nguniversal/express-engine --clientProject tracks
My goal once our apps are stable after this migration is to continue bringing the apps together by creating a single Angular application and copying (or referencing directly across projects). By using lazy loading, I can keep the app fast by loading modules as needed.
# from a directory such as Spotify/App...
# edit the AppModule.ts routes...
const routes: Routes = [
{
path: 'tracks',
loadChildren: import('../../Tracks/app/app.module')
.then(m => m.AppModule)
},
{
path: 'playlists',
loadChildren: import('../../Playlists/app/app.module')
.then( m => m.AppModule )
}
];
Micro-frontends were a mistake. There is so much overhead associated with updating/maintaining a large number of projects with nearly-identical architecture. We are desperate to reclaim our productivity. So much of our developer time is spent on the overhead of the project.
Less is more.
I hope this was helpful! I'll work on packaging up my migration scripts. Let me know if you have any further questions. I hope this helps. Thanks for reading.