RFC: Ivy Library Distribution
See original GitHub issueAuthors: Pete Bacon Darwin (@petebacondarwin), Alex Rickabaugh (@alxhub)
Status: Ended
tl;dr
We’re proposing an update to the Angular Package Format (APF), which defines an “Ivy-native” library format on NPM and introduces a new tool (the Angular Linker) to process libraries for inclusion in an application build. This linker will be available as a Babel plugin for maximum compatibility with all Angular build processes, and will be integrated into the Angular CLI.
This proposal replaces the current system of publishing libraries in View Engine format, and running ngcc, the Angular compatibility compiler, to upgrade them to Ivy after npm install
.
As part of upgrading the APF, we’re also proposing standardizing on ES2015 as a publication format.
Background
The Angular ecosystem is currently in a transitional period as applications update from the View Engine to the Ivy runtime. During this period it was essential that the library ecosystem on NPM remain compatible with View Engine applications. To achieve this, libraries are always built and published in “View Engine format”, which ensures compatibility with all existing View Engine applications, including those using Angular 9+ and opting out of Ivy.
To enable Ivy applications to consume these View Engine libraries, we introduced the Angular Compatibility Compiler (ngcc). ngcc runs as part of an Ivy application’s build process and updates any View Engine libraries in node_modules
to use the Ivy runtime. This allows the Ivy app to build against View Engine libraries.
As we deprecated View Engine runtime and will remove it completely in a future version of Angular, this compatibility is only needed temporarily, after which libraries can ship on NPM in a format that’s directly compatible with Ivy. This RFC proposes such an “Ivy-native” library format.
Requirements
We considered the following goals to be essential requirements which ensure the health and stability of the Angular ecosystem:
- Low-friction integration with non-CLI build tooling.
Many applications use the Angular CLI to build, but not all. Like with ngcc, it must be straightforward for non-CLI builds to consume Ivy libraries from NPM. Not all Angular projects are able to use the CLI.
For example, previously lazy loading in Angular was extremely difficult or impossible to achieve without using the Angular CLI (or the Webpack plugin) and even then it only worked with the @angular/router
. In Angular 8 lazy loading was supported via ES dynamic import(), which allowed for lazy loading of Angular modules and components independently of the toolchain in use.
- No in-place modification of node_modules.
A key aspect of ngcc’s design is that it updates libraries in-place in node_modules
. This was needed to ensure compatibility with existing build tooling, and because changes to library .d.ts files were required for downstream compilations.
Mutating node_modules
causes a number of problems: Package managers frequently overwrite such updates, requiring expensive re-runs of ngcc. Certain tools such as PNPM also rely on the immutability of node_modules
and are simply not compatible with ngcc as a result.
This was acceptable since ngcc is a temporary solution, but in the long term mutating node_modules
should not be a part of our strategy for libraries.
- Maintaining Angular’s guarantees of library compatibility.
Angular formally guarantees that a library built with a major version X will be compatible with an application using version X+1. For example, Angular 8 libraries can be used from applications built with Angular 9.
In practice, this compatibility often extends much further, and there have been very few breakages since the original Angular 2.0 release.
- Generated Ivy instructions do not become public API.
The code generation API of the Ivy runtime, like that of View Engine, should remain private API. That is, Ivy instructions should not be published to NPM, as that would severely limit Angular’s ability to evolve and optimize the runtime in the future.
Feedback Requested
The Angular team is interested in any and all feedback from our community regarding this proposal. There are a few aspects of this design where we feel this feedback will be especially valuable:
-
For application developers that use a custom build process instead of the Angular CLI, what challenges do you anticipate integrating the linker into your build process? How does this compare to using ngcc today? Would something other than a Babel plugin work well for you?
-
For application developers who use the UMD package format in some way (e.g. with server-side rendering), what challenges do you anticipate if Angular libraries no longer ship with UMD bundles? What could the Angular CLI do to alleviate those challenges?
-
For authors of Angular utilities that rely on
.metadata.json
information, what does your tooling do, and how are you using the metadata files? What could we provide to ensure your tools are still able to function?
Proposal
The compilation lifecycle of Angular libraries
An important aspect of Angular compilation in the previous View Engine architecture is that libraries have their NgFactory code generated at application build time, using the same version of the Angular compiler as the application itself. This has several important effects:
- It ensures libraries are compatible with a wide range of Angular versions, including future versions, which satisfies requirement 3.
- Code generated for View Engine NgFactory classes is never published to NPM, satisfying requirement 4.
At the same time, this approach requires that libraries are shipped on NPM with the metadata required to run Angular compilation later, during the application build. For View Engine, this is achieved through .metadata.json
files which are published alongside each JS file.
For Ivy libraries, we propose a modification of this architecture, which maintains its benefits while optimizing for application build efficiency. Instead of performing all compilation at application build time, Ivy libraries will be compiled in two phases:
Prior to publication on NPM, the library is compiled with the Ivy compiler in a special library mode. Instead of generating .metadata.json
files, the compiler collects metadata for each component and emits it into the component class itself, in place of the normal Ivy component definition.
During the application build, a “linker” tool processes each library file and completes compilation of each component, replacing the inline metadata with a full component definition.
Pre-publication compilation
Prior to publication on NPM, an Ivy library will be compiled by ngc
in a library mode. This operation analyzes the declared Angular types in the library (any components, directives, etc) and produces self-contained metadata which is inlined into the component class in place of the Ivy component definition.
Unlike with View Engine’s .metadata.json
files, the inlined component metadata is “self-contained” - it has all of the information needed to compile that component, without the need to reference any other files (such as the NgModule or any consumed directives/pipes/etc). This means that the final compilation step can be performed on a component-by-component basis, opening up lots of opportunities for caching and parallelization.
Application build “linking”
An application will install many Angular libraries from NPM into its node_modules
. The code in these libraries cannot be bundled directly along with the built application as it is not fully compiled. Compilation must be finished by running a new Angular tool which translates the inline metadata into final component definitions. We call this process “linking” and the tool the “Angular linker”.
The linker scans its inputs for inline Angular metadata, extracts it, and replaces it with a compiled component. Because this process happens during the application build, it uses the same version of the Angular compiler, ensuring that the app and all of its libraries are built with a single version of Angular.
The linker as a Babel plugin
To satisfy requirement 1, it makes sense to implement the linker as a Babel plugin. Babel has become a de facto standard for JS tooling, and babel plugins can be used in a variety of environments. Webpack and Rollup can both use Babel plugins, in addition to standalone Babel itself.
As a Babel plugin, the linker will receive an input AST corresponding to a particular library file, and transform it, replacing the inline metadata with a compiled component definition and maintaining the correct source mapping.
It’s also common for Babel plugins to support some kind of build caching. We can take advantage of this in the linker plugin to make repeat compilations virtually free. Effectively, then, libraries will only need to be processed by the linker a single time, regardless of other NPM operations (unlike ngcc which often needed to be re-run if NPM or yarn reverted changes ngcc made to node_modules
).
Integration with Angular Tooling
The Angular CLI will integrate the linker plugin automatically, so users of the CLI will be able to install Ivy-native libraries from NPM without any additional configuration.
On the publication side, ng-packagr will be updated with the capability to publish libraries in the Ivy-native format as well, meaning that any existing libraries which use it will be trivially able to switch over.
Removing UMD from the APF
As part of updating the Angular Package Format, we’re standardizing on ES2015 as the publication format for libraries. This means that UMD bundles will no longer be published as part of Angular packages.
ES5 support was dropped in the previous version of the APF, and Angular tooling now downlevels ES2015 sources on NPM to ES5 as needed. This approach has been working extremely well, and we feel that UMD could be handled in a similar way.
Using ES2015 as the “source format of truth” strikes a balance between flexibility for translation into other needed formats, and ease of use with most tooling and browsers able to consume ES2015 without modification (so post-processing isn’t required).
Alternatives Considered
This section summarizes potential alternative designs that were considered and rejected. See the linker design doc for more details.
Shipping instructions directly to NPM
It would theoretically be possible to compile libraries directly in Ivy mode, and ship the resulting artifacts to NPM directly. Downstream consumers would not need to perform any extra build steps to consume such libraries.
However, this idea runs into a number of issues with the requirements listed above:
- It complicates library compatibility, especially across major versions of Angular.
- It greatly increases the difficulty of changing the Angular runtime, since all runtime instructions would become “public API” of Angular.
Continuing to use ngcc
It’s possible that ngcc could be adapted to continue to work indefinitely. However, ngcc has several undesirable characteristics that fail to meet our requirements for a long-term library solution:
- It mutates node_modules, making it incompatible with a number of other tools in the JS ecosystem.
- It requires expensive processing that’s hard to cache, since it consumes entire packages as input instead of individual files.
Additional Resources
- Design doc for the Angular Linker concept
Issue Analytics
- State:
- Created 3 years ago
- Reactions:195
- Comments:54 (41 by maintainers)
Top GitHub Comments
With the current package format, being forced to support multiple formats like es5, umd and so on is our main pain point with Angular packages. We are building an internal, private library that is only ever being used by our own applications. Still we need to build all the other package formats that we are never going to use. This wastes compilation time and increases package size (with several CI builds daily, which all produce packages that are stored in our internal feed, that probably accumulates to a lot of wasted disk space over time).
We also don’t need view engine support any more.
It would be great to have only a single format like es2015 and ivy support.
I love that you’re citing my own talk for that 😄
And okay, that’s what I understood you to mean. I asked because in the context of
ng serve
, we have another concept of “incremental compilation” which refers to how much of the previous build can be used in the subsequent one, at the TypeScript level.You’re right, it’s not 100% “no work remaining”, but in practice the two should be virtually indistinguishable since the “linking” operation is really a post-install step for a library, not a necessary part of every build.