The Angular Compiler (which we call ngc) is the tool used to compile Angular applications and libraries. ngc is built on the TypeScript compiler (called tsc) and extends the process of compiling TypeScript code to add additional code generation related to Angular’s capabilities.Angular’s compiler serves as a bridge between developer experience and run time performance: Angular users author applications against an ergonomic, decorator-based API, and ngc translates this code into more efficient runtime instructions.For example, a basic Angular component may look something like this:https://medium.com/media/7b55cef8967a018219c2722933fe27ba/hrefAfter compilation by ngc, this component instead looks like:https://medium.com/media/de4d3211b798a516195aad02754b3578/hrefThe @Component decorator has been replaced with several static properties (ɵfac and ɵcmp), which describe this component to the Angular runtime and implement rendering and change detection for its template.In this way, ngc can be considered an extended TypeScript compiler which also knows how to “execute” Angular decorators, applying their effects to the decorated classes at build time (as opposed to run time).Inside ngcngc has several important goals:
- Compile Angular decorators, including components and their templates.
- Apply TypeScript’s type-checking rules to component templates.
- Re-compile quickly when the developer makes a change.
Let’s examine how ngc manages each of these goals.Compilation FlowThe main goal of ngc is to compile TypeScript code while transforming recognized Angular decorated classes into more efficient representations for run time. The main flow of Angular compilation proceeds as follows:
- Create an instance of the TypeScript compiler, with some additional Angular functionality.
- Scan every file in the project for decorated classes, and build a model of which components, directives, pipes, NgModules, etc. need to be compiled.
- Make connections between decorated classes (e.g. which directives are used in which component templates).
- Leverage TypeScript to type-check expressions in component templates.
- Compile the whole program, including generating extra Angular code for every decorated class.
Step 1: Creating the TypeScript programIn TypeScript’s compiler, a program to be compiled is represented by a ts.Program instance. This instance combines the set of files to be compiled, type information from dependencies, and the particular set of compiler options to be used.Identifying the set of files and dependencies is not straightforward. Often, the user specifies one “entrypoint” file (for example, main.ts), and TypeScript must look at the imports in that file to discover other files that need to be compiled. Those files have additional imports, which expand to even more files, and so on. Some of these imports point to dependencies: references to code that’s not being compiled, but is used in some way and needs to be known to TypeScript’s type system. These dependency imports are to .d.ts files, usually in node_modules.At this step, the Angular compiler does something special: it adds additional input files to the ts.Program. For every file written by the user (e.g. my.component.ts), ngc adds a “shadow” file with an .ngtypecheck suffix (e.g., my.component.ngtypecheck.ts). These files are used internally for template type-checking (more on that later).Depending on compiler options, ngc may add other files to the ts.Program, such as .ngfactory files for backwards compatibility with the previous View Engine architecture.Step 2: Individual AnalysisIn the analysis phase of compilation, ngc looks for classes with Angular decorators, and attempts to statically understand each decorator. For example, if it encounters an @Component decorated class, it looks at the decorator and attempts to determine the component’s template, its selector, view encapsulation settings, and any other information about the component which might be needed to generate code for it. This requires the compiler to be capable of an operation known as partial evaluation: reading expressions within decorator metadata and attempting to interpret those expressions without actually running them.Partial EvaluationSometimes information in an Angular decorator is hidden behind an expression. For example, a selector for a component is given as a literal string, but it could also be a constant:https://medium.com/media/fc344a00615ed4f9bed8b296c84e760a/hrefngc uses TypeScript’s APIs for navigating code to evaluate the expression MY_SELECTOR, tracing it back to its declaration and eventually resolving it to the string ‘my-cmp’. The partial evaluator can understand simple constants; object and array literals; property accesses; imports/exports; arithmetic and other binary operations; and even evaluate calls to simple functions. This feature gives Angular developers more flexibility in how they describe components and other Angular types to the compiler.Output of AnalysisAt the end of the analysis phase, the compiler already has a good picture of what components, directives, pipes, injectables, and NgModules are in the input program. For each of these, the compiler constructs a “metadata” object describing everything it learned from the class’ decorators. By this point, components have had their templates and stylesheets loaded from disk (if necessary), and the compiler may already have produced errors (known in TypeScript as “diagnostics”) if semantic errors are detected in any part of the input so far.Step 3: Global AnalysisBefore it can type-check or generate code, the compiler needs to understand how the various decorated types in the program relate to each other. The primary goal of this step is to understand the NgModule structure of the program.NgModulesTo type-check and generate code, the compiler needs to know which directives, components, and pipes are used in each component’s template. This isn’t straightforward because Angular components don’t directly import their dependencies. Instead, Angular components describe templates using HTML, and potential dependencies are matched against elements in those templates using CSS-style selectors. This enables a powerful abstraction layer: Angular components do not need to know exactly how their dependencies are structured. Instead, each component has a set of potential dependencies (its “template compilation scope”), only a subset of which will end up matching elements in its template.This indirection is resolved through the Angular @NgModule abstraction. NgModules can be thought of as composable units of template scope. A basic NgModule may look like:https://medium.com/media/629c71847128b6568956f5a044be879a/hrefNgModules can be understood as each declaring two different scopes:
- A “compilation scope”, representing the set of potential dependencies that are available to any components declared in the NgModule itself.
- An “export scope”, representing a set of potential dependencies that are made available in the compilation scope of any NgModules which imports the given NgModule.
In the above example, ImageViewerComponent is a component declared in this NgModule, so its potential dependencies are given by the compilation scope of the NgModule. This compilation scope is the union of all declarations and the export scopes of any NgModules which are imported. Because of this, it’s an error in Angular to declare a component in multiple NgModules. Additionally, a component and its NgModule must be compiled at the same time.In this case, CommonModule is imported, so the compilation scope of ImageViewerModule (and thus ImageViewerComponent) includes all of the directives and pipes exported by CommonModule — NgIf, NgForOf, AsyncPipe, and a half dozen others. The compilation scope also includes both declared directives — ImageViewerComponent and ImageResizeDirective.Note that for components, their relationship to the NgModule which declares them is bi-directional: the NgModule both defines the component’s template scope as well as makes that component available in the template scopes of other components.The above NgModule also declares an “export scope” consisting of the ImageViewerComponent alone. Other NgModules which import this one will have ImageViewerComponent added to their compilation scopes. In this way, the NgModule allows for encapsulation of the implementation details of ImageViewerComponent — internally, it might use the ImageResizeDirective, but this directive is not made available to consumers of ImageViewerComponent.To determine these scopes, the compiler builds a graph of NgModules, their declarations, and their imports and exports, using the information it learned about each class individually from the prior step. It also requires knowledge about dependencies: components and NgModules imported from libraries and not declared in the current program. Angular encodes this information in the .d.ts files of those dependencies..d.ts metadataDuring the global analysis phase, the Angular compiler needs to fully enumerate the compilation scope of NgModules declared in the compilation. However, these NgModules may import other NgModules from outside the compilation, from libraries and other dependencies. TypeScript learns about types from such dependencies through declaration files, which have the extension .d.ts. The Angular compiler uses these .d.ts declarations to pass along information about Angular types within those dependencies.For example, the above ImageViewerModule imports CommonModule from the @angular/common package. Partial evaluation of the imports list will resolve the classes named in imports to declarations within the .d.ts files from those dependencies.Just knowing the symbol of imported NgModules is not sufficient. To build its graph, the compiler passes information about the declarations, imports, and exports of NgModules through the .d.ts files in a special metadata type. For example, in the generated declaration file for Angular’s CommonModule, this (simplified) metadata looks like:https://medium.com/media/7aa74de9820d20fdab1128399465c44a/hrefThis type declaration is not intended for type-checking by TypeScript, but instead embeds information (references and other metadata) about Angular’s understanding of the class in question into the type system. From these special types, ngc can determine the export scope of CommonModule. Using TypeScript’s APIs to resolve the references within this metadata to those class definitions, it can extract useful metadata regarding the directives/components/pipes themselves:https://medium.com/media/9ba4d999bf4f9ed1d807a51210396ae7/hrefThis gives ngc sufficient information about the structure of the program to proceed with compilation.Step 4: Template Type-Checkingngc is capable of reporting type errors within Angular templates. For example, if a template attempts to bind a value {{name.first}} but the name object does not have a first property, ngc can surface this issue as a type error. Performing this checking efficiently is a significant challenge for ngc.TypeScript by itself has no understanding of Angular template syntax and cannot type-check it directly. To perform this checking, the Angular compiler converts Angular templates into TypeScript code (known as a “Type Check Block”, or TCB) that expresses equivalent operations at the type level, and feeds this code to TypeScript for semantic checking. Any generated diagnostics are then mapped back and reported to the user in the context of the original template.For example, consider a component with a template that uses ngFor:https://medium.com/media/c63c7e1e2b8f699008952d565fb313af/hrefFor this template, the compiler wants to check that the user.name property access is legal. To do this, it must first understand how the type of the loop variable user is derived through the NgFor from the input array of users.The Type Check Block that the compiler generates for this component’s template looks like:https://medium.com/media/1cd7237d5248ef3048333cd1ed4dde2c/hrefThe complexity here appears to be high, but fundamentally this TCB is performing a specific sequence of operations:
- It first infers the actual type of the NgForOf directive (which is generic) from its input bindings. This is named _t1.
- It validates that the users property of the component is assignable to the NgForOf input, via the assignment statement _t1.ngForOf = ctx.users.
- Next, it declares a type for the embedded view context of the *ngFor row template, named _t2, with an initial type of any.
- Using an if with a type guard call, it uses NgForOf’s ngTemplateContextGuard helper function to narrow the type of _t2 according to how NgForOf works.
- The implicit loop variable (user in the template) is extracted from this context and given the name _t3.
- Finally, the access _t3.name is expressed.
If the access _t3.name is not legal by TypeScript’s rules, TypeScript will produce a diagnostic error for this code. Angular’s template type-checker can look at the location of this error in the TCB and use the embedded comments to map the error back to the original template before showing it to the developer.Since Angular templates contain references to component class properties, they have types from the user’s program. Thus, template type-checking code cannot be checked independently, and must be checked within the context of the user’s whole program (in the above example, the component type is imported from the user’s test.ts file). ngc accomplishes this by adding the generated TCBs to the user’s program through an TypeScript incremental build step (generating a new ts.Program). To avoid thrashing the incremental build cache, type-checking code is added to separate .ngtypecheck.ts files that the compiler adds to the ts.Program on creation rather than directly to user files.Step 5: EmitWhen this step begins, ngc has both understood the program and validated that there are no fatal errors. TypeScript’s compiler is then told to generate JavaScript code for the program. During the generation process, Angular decorators are stripped away and several static fields are added to the classes instead, with the generated Angular code ready to be written out into JavaScript.If the program being compiled is a library, .d.ts files are also produced. The files contain embedded Angular metadata that describes how a future compilation can use those types as dependencies.Being Fast IncrementallyIf the above sounds like a lot of work to go through prior to generating code, that’s because it is. While TypeScript and Angular’s logic is efficient, it can still take several seconds to perform all of the parsing, analysis, and synthesis required to produce JavaScript output for the input program. For this reason, both TypeScript and Angular support an incremental compilation mode, where work done previously is reused to more efficiently update a compiled program when a small change is made in the input.The main problem of incremental compilation is: given a specific change in an input file, the compiler needs to determine which outputs may have changed, and which outputs are safe to reuse. The compiler must be perfect and err on the side of recompiling an output if it cannot be certain that it has not changed.To solve this problem, the Angular compiler has two main tools: the import graph and the semantic dependency graph.Import graphAs the compiler is performing partial evaluation operations while analyzing the program for the first time, it builds a graph of critical imports between files. This allows the compiler to understand dependencies between files when something changes.For example, if the file my.component.ts has a component, and that component’s selector is defined by a constant imported from selector.ts, the import graph shows that my.component.ts depends on selector.ts. If selector.ts changes, the compiler can consult this graph and know that my.component.ts’s analysis results are no longer correct and must be re-done.The import graph is important for understanding what might change, but it has two major issues:
- It’s overly sensitive to unrelated changes. If selector.ts is changed, but that change only adds a comment, then my.component.ts doesn’t really need to be recompiled.
- Not all dependencies in Angular applications are expressed via imports. If the selector of MyCmp does change, then other components which use MyCmp in their template might be affected, even though they never import MyCmp directly.
Both of these issues are addressed via the compiler’s second incremental tool:Semantic dependency graphThe semantic dependency graph picks up where the import graph leaves off. This graph captures the actual semantics of compilation: how components and directives relate to each other. Its job is to know which semantic changes would require a given output to be reproduced.For example, if selector.ts is changed, but the selector of MyCmp doesn’t change, then the semantic dep graph will know that nothing which semantically affects MyCmp has changed, and MyCmp’s previous output can be reused. Conversely, if the selector does change, then the set of components/directives used in other components may change, and the semantic graph will know that those components need re-compiling.IncrementalityBoth graphs therefore work together to provide fast incremental compilation. The import graph is used to determine which analysis needs to be re-done, and the semantic graph is then applied to understand how changes in analysis data propagate through the program and require outputs to be recompiled. The result is a compiler that can efficiently react to changes in inputs, and only do the minimum amount of work to correctly update its outputs in response.SummaryAngular’s compiler leverages the flexibility of TypeScript’s compiler APIs to deliver correct and performant compilation of Angular templates and classes. Compiling Angular applications allows us to offer a desirable developer experience in the IDE, to give build-time feedback about issues in the code, and to transform that code during the build process into the most efficient JavaScript to run in the browser.How the Angular Compiler Works was originally published in Angular Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.