owned this note
owned this note
Published
Linked with GitHub
# Angular in Monorepos
A "monorepo" is a single codebase (e.g. a single Git repository) that contains source code for multiple applications or libraries, often developed by different teams. Monorepos have a number of advantages as a project structure:
1. Since all code is built at the current HEAD of the repository, traditional complexity associated with versioning of APIs and libraries, or dependency management is gone.
2. Continuous Integration systems can verify that a change to a library will not break any dependent apps, without complex integration environments.
3. Large-scale refactoring changes can be performed as a single commit across multiple apps and libraries.
Angular supports monorepo project structures with the right configuration.
:::info
If you use the Angular CLI to generate applications and libraries in a monorepo, the builds will all be configured correctly and you don't have to care about anything listed in this document.
:::
:::warning
This document discusses monorepo configuration with Angular in the context of Angular v9 and the "Ivy" renderer. It might not be applicable to previous versions of Angular.
:::
## Different types of Angular libraries
There are two broad types of Angular libraries which could be present in a monorepo, "publishable" and "private".
### Publishable libraries
Publishable libraries are intended to be distributed on NPM or a private package repository, and must conform to the Angular Package Format (APF) specification for libraries. One of the main constraints of the APF is that the library must export all of its public-facing types (such as NgModules or Components) via one or more "entrypoints". Deep imports into APF libraries are not allowed.
If the library is built using the Angular CLI, it will be a publishable library in the Angular Package Format.
If not using the CLI, the `ng-packagr` tool can be used to prepare a library in the APF for publishing.
:::info
This document is concerned with proper configuration of dependencies within the monorepo itself. It does not cover the configuration of the CLI or `ng-packagr` to package and publish specific libraries. For this, see the CLI or `ng-packagr` docs.
:::
### Private libraries
Private (non-publishable) libraries are never intended for distribution outside of the repository in which they live. Private libraries are instead depended on by one or more applications within the monorepo itself. They can serve a number of purposes:
1. As a way to share code between multiple apps.
2. To split an application build up into multiple pieces, in order to speed up or parallelize the overall build.
3. To allow for a more granular development & testing cycle by only working on or testing a smaller part of the app.
Unlike publishable libraries, private libraries have no special requirement to export their API via entrypoints. Deep imports into private libraries are legal, and may even be the desired approach.
## Composition of an Angular monorepo
There are several aspects of configuration required to build Angular projects within a monorepo.
### Editor configuration
For all but the largest monorepos, it's often desirable to open the entire repository in an IDE, with seamless code navigation between individual projects.
This can be accomplished by configuring a single, top-level `tsconfig.json`, which the IDE will use when loading the project.
In this tsconfig, TypeScript options common to the entire monorepo (strictness settings, etc) can be specified.
Most importantly, path mappings should be configured which map each library's module specifier (the name used to import the library, e.g. `'@angular/cdk'`) to the library's `.ts` files.
For example, the Angular monorepo contains a number of publishable libraries, including `@angular/core` and `@angular/common`, in the following layout:
```
packages/
core/
index.ts
src/
...
common/
index.ts
src/
...
```
The code for `@angular/core` lives in `packages/core`, and the code for `@angular/common` lives in `packages/common`.
A top-level `tsconfig.json` for this repo might look like:
```jsonld=
{
"compilerOptions": {
"baseUrl": ".",
"outDir": "dist",
"paths": {
"@angular/core": ["./packages/core"],
"@angular/core/*": ["./packages/core/*"],
"@angular/common": ["./packages/common"],
"@angular/common/*": ["./packages/common/*"],
},
...
}
}
```
For each package, two path mappings are needed. The first maps the top-level package, the second maps files within it.
Alternatively, a `tsconfig.json` for this repo might specify a single mapping for the entire `@angular` namespace. This is possible because the Angular repo follows the convention that the code for library `@angular/X` is under the directory `packages/X`. This would look like:
```jsonld
"compilerOptions": {
"paths": {
"@angular/*": ["./packages/*"],
},
}
```
No top-level mapping is needed because it is impossible to import `'@angular'` by itself.
### Build configuration
:::info
As mentioned above, this document is concerned with the build relationship between different projects in the monorepo. It does not cover packaging of publishable libraries via `ng-packagr`.
:::
When it comes time to build a library or application, the editor configuration is not sufficient. Each project within the monorepo requires a separate tsconfig which specifies the _build_ options for that particular project. Often a project will have more than one configuration, such as a development build config and a production build config.
:::warning
These tsconfigs should not be named `tsconfig.json` as they will then be picked up by the editor, incorrectly overriding the top-level configuration. Instead, `tsconfig.build.json` or a similar name can be used.
:::
The build tsconfig has several responsibilities:
* Configuring TypeScript options for the build.
* Declaring the inputs to the compilation.
* Configuring the output of build artifacts.
* Configuring path mappings for dependencies.
Often it's desirable for each build configuration to extend the top-level editor tsconfig. In addition to saving lots of repetition, this setup ensures the editor and the compiler agree on the set of build options in use, and thus show similar errors to the user.
#### Inputs configuration
Specify inputs is done with the `files` option in tsconfig:
```jsonld=
// packages/core/tsconfig.build.json
{
"extends": "../../tsconfig.json",
"files": {
"./index.ts"
}
}
```
This should always be configured to limit the set of TS files being passed into the compilation, and only include files which belong to the particular project being compiled.
#### Output configuration
It's highly recommended (but not necessary) to emit output artifacts into a "parallel" filesystem tree - one that matches the repository's own directory layout. Often this directory is called `dist`.
Using a combination of the `outDir` and `rootDir` options in tsconfig, the `@angular/core` library located in `packages/core` can be configured to output to `dist/packages/core`:
```jsonld=
// packages/core/tsconfig.build.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"outDir": "../../dist/packages/core",
"rootDir": ".",
},
"files": {
"./index.ts"
}
}
```
`outDir` here configures the output directory for the built library. The `packages/core` directory structure is mirrored into the `dist/` output directory. The `rootDir` setting guarantees that regardless of how this build is invoked, `packages/core` will always end up being compiled to `dist/packages/core`.
#### Path mappings of dependencies
If the build tsconfig for a project extends the top-level editor tsconfig, it will include the path mappings defined there for all libraries. However, this is a problem: these path mappings map each library to its _source_ files. This is correct for the IDE, but when actually building a project Angular needs to receive its dependencies in their _compiled_ form.
For example, `@angular/common` depends on `@angular/core`, so the `tsconfig.build.json` for `@angular/common` needs to map the `@angular/core` path to its compiled form in `dist`.
```jsonld=
// packages/common/tsconfig.build.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"outDir": "../../dist/packages/common",
"rootDir": ".",
"paths": {
"@angular/core": ["../../dist/packages/core"],
"@angular/core/*": ["../../dist/packages/core/*"],
}
},
"files": {
"./index.ts"
}
}
```
As before, two path mappings are needed: one for the top level and one for any subpaths.
Unlike the editor configuration, this build tsconfig's path mappings point to the `dist` version of `@angular/core`. As a result, it's a prerequisite to build `@angular/core` before `@angular/common`, since the latter build depends on the output of the former.
#### Bazel
A build system like Bazel can really streamline this kind of setup. Bazel will automatically construct the needed tsconfigs, avoiding the need to manage this configuration yourself. Additionally, it understands the dependency structure of the projects in the monorepo, and will automatically build whatever projects need to be built in the correct order.
## Private libraries and import paths
An interesting issue arises when an application compilation is split into multiple libraries.