This article has participated in the activity of “New person creation Ceremony”, and started the road of digging gold creation together.

As a front-end framework designed for “big front-end projects,” Angular has a lot of design to learn from, and this series focuses on how those designs and features work. This article first introduces the overall design of the Ivy compiler, which is the core feature of Angular.

The template compiler (renderer) is a core capability for a front-end framework. A new template compiler, the Ivy compiler, was introduced in Angular 8.0. Until now, Angular used View Engine to compile templates.

Ivy compiler capability

The purpose of a compiler is basically to compile code written by a developer into code that can be run in a browser. Using a compiler, the front-end framework can define a lot of its own syntax and add performance optimization, security checks and other features to the code during compilation. For Angular, the compiler also needs to support both AOT and JIT compilation of developer code.

Angular refactored the compiler and named it Ivy, which is very important for the Angular framework, similar to how React refactored Fiber.

Ivy new features

Let’s start by looking at some of the features of the Ivy compiler, including but not limited to the following:

  • 🚀 Shorten build time (add incremental compilation)
  • 🔥 achieves a better build size (the generated code is more compatible with tree-shaking), effectively reducing the code package size
  • 🔓 unlocks new potential features (meta-programming or higher-level components, support for lazy loading of components, support for new change-detection systems that are not zone.js based, etc.)

The previous sections covered Angular metaprogramming, component-module relationships, and design and introduction in zone.js, many of which cannot be separated from the design and introduction of the Ivy compiler. For example, Angular dependency design has a bug introduced by deferred modules. We discussed dependency injection in Angular by dividing injectors into element injectors and module injectors, while Ivy compiler uses Node lazy-loading support at component level. Finally, the problem of repeated loading of delayed modules is solved.

Today we’ll take a look at the overall design of the Ivy compiler, followed by specific sections detailing some of the internal source code implementation.

Ivy Architecture Design

In Angular, developers write mostly Typescript code that includes a lot of Angular apis and syntax sugar, so Angular needs to parse to AST. Compiling from the AST into code that eventually runs in the browser is the core capability that the Ivy compiler needs to implement.

The Ivy compiler consists of two main parts:

  1. ngtscIs a typescript-to-javascript compiler that converts Angular decorators into static properties. It is a minimal wrapper that wraps aroundtscOutside,tscContains a series of Angular transforms.
  2. ngccThe main responsibility is to process the code from NPM and generate the equivalent version of Ivy, as usedngtscSame as compiling code.

Template compilation

To compile the template using TemplateCompiler in the Ivy compiler, do the following:

  1. Tag templates.
  2. Parsing the markup content into an HTML AST.
  3. Convert HTML AST to Angular template AST.
  4. Convert Angular template AST to template functions.

When Angular Template AST transforms and annotates the HTML AST version, it does the following:

  1. Link Angular template syntax shortcuts (for example*ngForand[name]) to its specification version (andbind-name).
  2. Collect references (#Properties) and variables (let-Properties).
  3. Binding expressions in the binding expression AST are parsed and transformed using the collected variables and references.

In addition to the above, the procedure generates an exhaustive list of selector targets, including potential targets for the selectors of any component, instruction, or pipe. Determining that a component contains a list of components, directives, and pipes on which it depends lets you know at run time which components and directives are applied to elements and which pipes are referenced by binding expressions. Thus TemplateCompiler can generate template functions from strings without additional information.

The process of determining this list is called reference inversion, because it reverses links from modules (containing dependencies) to components to links from components to their dependencies. The program then only needs to include the types that the original components that are rendered depend on and any types needed for those dependencies. In addition, tree-shaking is addressed.

Typescript parser

To implement AST parsing and transformation, you need parsers. For Typescript code, the compiler flows as follows:

|------------| |----------------------------------> | TypeScript | | | .d.ts | | |------------| | |------------| |-----|  |-----| |------------| | TypeScript | -parse-> | AST | ->transform-> | AST | ->print-> | JavaScript | | source | | |-----| | |-----| | source | |------------| | | | |------------| | type-check | | | | | v | | |--------| | |--> | errors  | <---| |--------|Copy the code

The resolution step is a traditional recursive descent parser, enhanced to support incremental resolution, which emits an abstract syntax tree (AST). Transformation steps are a set of AST to AST transformations that perform various tasks, such as removing type declarations, lowering module and class declarations to ES5, converting async methods to state machines, and so on.

Compiler design

As mentioned earlier, Ivy supports incremental compilation to reduce build times. The expectation of incremental compilation is that when a library is already compiled, we don’t have to recompile it every time, but rather recompile it based on the parts that have changed. While this may seem simple, it actually presents a considerable challenge to the compiler, because component generation code may use the internal details of another component.

In broad terms, the Ivy model compiles Angular decorators as static properties on a class, including:

  • Component compilation (ViewCompilerAnd style compiler) : compile@Component= >ɵ ɵ defineComponent
  • Pipeline to compilePipeCompilerCompile:@Pipe= >ɵ ɵ definePipe
  • Instruction compilationDirectiveCompilerCompile:@Directive= >ɵ ɵ defineDirective
  • Injectable compilationInjectableCompilerCompile:@Injectable= >ɵ ɵ defineInjectable
  • Modules compiledNgModuleCompilerCompile:@NgModule= >ɵ ɵ defineInjector(ɵ ɵ defineNgModuleIn JIT only)

These operations must be done without global program data and, in most cases, only with individual decorator data.

Therefore, the Ivy compiler must not rely on any input that is not passed directly to it (for example, it must not scan the source or other data in the metadata). This restriction is important for two reasons:

  1. Because you can see all the input from the compiler, it helps enforce the Ivy locality principle.
  2. It can prevent in--watchBuild incorrectly in mode because dependencies between files are easy to track.

So in Ivy, every “compiler” that converts a single decorator into a static field will act as a “pure function.” Given input metadata about a particular type and decorator, it generates an object that describes the field to be added to that type, as well as the initialized value for that field (in AST format).

For example, the input to the @Component compiler includes the following:

  • A reference to a component class
  • Template and style resources for the component
  • Component selector
  • Selector mapping of the module to which the component belongs

Ivy compilation model

In Angular, the logic for instantiating components, creating DOM nodes, and running change detection is implemented as an atomic unit called the Angular interpreter. The compiler only generates metadata about the components and elements defined in its templates.

In older versions of View Engine, the compilation process was:

My name is {{name}}

viewDef(0,[
    elementDef(0.null.null.1.'span',...). , textDef(null['My name is '. ] )]Copy the code

In Ivy, the compiled code looks something like this:

// create mode
if (rf & RenderFlags.Create) {
  elementStart(0."span");
  text(1);
  elementEnd();
}
// update mode
if (rf & RenderFlags.Update) {
  textBinding(1, interpolation1("My name is", ctx.name));
}
Copy the code

In the Ivy compiler, the compilation process is:

In View Engine, component definitions (template data) reside in their own files, independent of the component class. In the Ivy compiler, component definitions are attached to component classes through static fields, and separate files are not created during compilation.

conclusion

Ivy compiler DESIGN DOC(Ivy) Is the best compiler for Angular compiler DESIGN DOC(Ivy): The Compiler Architecture.

The Ivy compiler is a core Angular capability that can’t be summarized in a single article. This article does not cover the NGTSC compilation process, resource loading, etc., nor does it begin to explore its implementation with source code from Angular. I will try to explore these bits and pieces in the future, hoping to learn more than just architecture documentation, and I will try to document my own learning process and share it with those of you who are interested in Angular

reference

  • DESIGN DOC(Ivy): Compiler Architecture
  • Under the hood of the Angular Compatibility Compiler (ngcc)
  • What is Angular Ivy?
  • All you need to know about Ivy, The new Angular engine!
  • Ivy engine in Angular: first in-depth look at compilation, runtime and change detection
  • Everything you need to know about change detection in Angular