Skip to content

Instantly share code, notes, and snippets.

@chuckjaz
Last active June 19, 2019 07:22
Show Gist options
  • Star 39 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save chuckjaz/65dcc2fd5f4f5463e492ed0cb93bca60 to your computer and use it in GitHub Desktop.
Save chuckjaz/65dcc2fd5f4f5463e492ed0cb93bca60 to your computer and use it in GitHub Desktop.
Documentation for Angular Metadata

Angular Metadata

Ahead-of-time complilation uses the declarations and annotations to pre-compile the templates in your application into the JavaScript instead transformation them at runtime. This allows your applications to load faster and requires less of the Angular framework to be bundled with your application.

To accomplish this the annotations that define your components and directives need to be extracted from your source code in a way that the compiler can digest it. This document describes how this is done and describes the subset of JavaScript that can be used in annotations that are AoT compliant.

Just-in-time (JIT) compile

A typical component declaration might look like the following:

@Component({
  selector: 'typical',
  template: '<div>A typical component for {{data.name}}</div>'
})
class TypicalComponent {
  @Input() data: MyData;
}

When this component is used, instances of this component are created by calling the factory for TypicalComponent. In JIT mode the factory function is created at runtime when application (or lazy loaded module) that uses the component is loaded in the page.

The Component annotation records the selector and template specified for the TypicalComponent using the Reflect API. The JIT compiler will consults the information recorded using Reflect to produce the factory function.

This inforation is used by the compiler to generate a factory function that creates a constructor function that includes code like:

self._el_0 = self.renderer.createElement(parentRenderNode,'div',null);
self._text_1 = self.renderer.createText(self._el_0,'',null);
self._expr_0 = jit_UNINITIALIZED3;

which creates the div and text node that are included in the template.

It also generates detect changes function detects if the expression data.name changes and updates the elements, in place.

var currVal_0 = jit_interpolate4(1,'A typical component for ',self.context.data.name,'');
if (jit_checkBinding5(throwOnChange,self._expr_0,currVal_0)) {
  self.renderer.setText(self._text_1,currVal_0);
  self._expr_0 = currVal_0;
}

Whenever instances of TypicalComponent are needed, this factory function is called to create one. This factory function does not change during the life of your application but you page load will wait until it is created (and all the other components referenced by the application are created) to being to render the content of the page.

Ahead-of-time (AoT) compile

Ahead-of-time compilation runs the compiler when bundling the application, instead of when the page loads.

When using AoT, the Reflect API is not available and even if it was, running the code to populate Reflect above could have side-effects or rely elsewhere in your application that would only run correctly in a web page. What is needed is a way to record what is in the annotation without having to execute it. This is where Angular metadata comes in.

Part of what ngc does when it translates your modules into JavaScript is to create a corresponding .metadata.json file for each .d.ts file. The .metadata.json file contains the information recorded in the Angular decorators so the same information available at runtime is avaiable when building the application. The annotations are translated into JSON format of an abstract syntax tree (AST). The schema for the AST is described in TypeScript interfaces in Angular's schema.ts.

The .metadata.json does not try to interpret the meaning of the attribute, it just records a representation of the source. The interpretation is done by the StaticReflector as part of ngc discussed below. The StaticReflector places additional restrictions on what the can be interpreted.

Metadata restrictions

The metadata supports a wide subset of JavaScript, but it is a subset. Not everything that can be supported by JIT can be supported by AoT. The restrictions mainly come from what can be represented in the .metadata.json file and what can be interpreted by the StaticReflector.

In general, the restrictions are,

  1. The metadata must be an expression in the supported JavaScript subset.
  2. All unfolded reference must be to export symbol.
  3. Only calls of functions that are supported by the StaticReflector.

It is important to note that more can be encoded by the collector than can be interpreted by the StaticReflector. This is especially true for call and new expressions. Only a small subset of functions and classes can be called. This is enumerated in more detail below is the section about the StaticReflector.

Supported expression subset

The subset of JavaScript supported by the collector includes:

Syntax Example
Literal object {cherry: true, apple: true, mincemeat: false}
Literal array ['cherries', 'flour', 'sugar']
Spread in literal array ['apples', 'flour', ...the_rest]
Calls bake(ingredients)
New new Oven()
Property access pie.slice
Array index ingredients[0]
Identifier reference Component
A template string `pie is ${multiplier} times better than cake`
Literal string 'pi'
Literal number 3.14153265
Literal boolean true
Literal null null
Supported prefix operator !cake
Supported Binary operator a + b
Conditional operator a ? b : c
Parenthisis (a + b)

If the expression uses syntax that is not supported then an error symbol will be written to the metadata. The StaticReflector will report the error if the metadata is used to interpret the content of an annotation.

If the metadata generated for a class would contain an error symbol then ngc can report that as an error instead of generating an potentially unusable .json.metadata file. See the strictMetadataEmit option for ngc. This is especially useful when building Angular libraries and is used to produce the .metadata.json files shipped as part of Angular.

Notable ommisions from this list include function expressions and lambdas. Including a function expression or a lambda in an annotation will report an error reqesting the lambda or function expression be turned into an exported function. For example, if you have the component annotation:

@Component({
  ...
  providers: [{provide: server, useFactory: () => new Server()}]
})

the collector will generate an error node in place of the lambda and, when interpreted by the StaticReflector will generate an error. To avoid this you can convert this to:

export function serverFactory() {
  return new Server();
}

@Component({
  ...
  providers: [{provide: server, useFactory: serverFactory}]
})

Even though function calls (including using new) can be encoded by the collector, only a small number of functions are supported by the StaticReflector.

If the static reflector trys to interpret a function it does not recognize an error will be produced.

The full list of supported functions and classes are listed below.

Folding

Only references to exported symbols can be resolved by the StaticReflector but the collector allows limited use of non-exported symbols through folding. A folded expression is one that is evaluated during collection and the evaluated result is encoded instead of the expression's AST. For example, expression 1 + 2 + 3 + 4 is foldable into 10. In addition, references to module local const declarations and initialized var and let declarations are foldable. For example, the following class,

const template = '<div>{{hero.name}}</div>';

@Component({
  selector: 'hero-name',
  template: template
})
class HeroComponent {
  @Input() hero: Hero;
}

is collectable because the module local reference to template can be substituted for its value and it is processed as if it was:

@Component({
  selector: 'hero-name',
  template: '<div>{{hero.name}}</div>'
})
class HeroComponent {
  @Input() hero: Hero;
}

Expressions can be folded:

const template = '<div>{{hero.name}}</div>';

@Component({
  selector: 'hero-name',
  template: template + '<div>{{hero.title}}</div>'
})
class HeroComponent {
  @Input() hero: Hero;
}

and the template value would be emitted as '<div>{{hero.name}}</div><div>{{hero.title}}</div>'.

Foldable syntax

The following table describes what expressions that are supported by the collector can be folded:

Syntax Foldable
Litearl object yes
Literal array yes
Spread in literal array no
Calls no
New no
Property access yes if target is foldable
Array index yes if target and index are foldable
Identifier reference yes if it is a reference to a local
A template with no substitutions yes
A template with substitutions yes if the substitions are foldable
Literal string yes
Literal number yes
Literal boolean yes
Literal null yes
Supported prefix operator yes if operand is foldable
Supported Binary operator yes if both left and right are foldable
Conditional operator yes if condition is foldable
Parenthisis yes if the expression is foldable

It an expression is not foldable it is written out as an AST and it is left for the StaticReflector to resolve.

At runtime, during JIT compile, the compiler will use the Reflect API to retrieve the information supplied in the Angular annotations. In particular it uses the Reflector which is an abstraction over the Reflect API.

During AoT the compiler uses the StaticReflector which is an implementation of Reflector that uses the information recorded in .metadata.json files and TypeScript instead of the Reflect API.

StaticReflector restrictions

The static reflector can evaluate all the syntax forms supported by the collector but it can only support calls and new expressions referencing a strict subset of symbols that the StaticReflector has built-in support for.

New

Symbol Module
OpaqueToken @angular/core

Annotations

Symbol Module
Attribute @angular/core
Component @angular/core
ContentChild @angular/core
ContentChildren @angular/core
Directive @angular/core
Host @angular/core
HostBinding @angular/core
HostListener @angular/core
Inject @angular/core
Injectable @angular/core
Input @angular/core
NgModule @angular/core
Optional @angular/core
Output @angular/core
Pipe @angular/core
Self @angular/core
SkipSelf @angular/core
ViewChild @angular/core

Calls

Symbol Module
trigger @angular/core
state @angular/core
transition @angular/core
style @angular/core
animate @angular/core
keyframes @angular/core
sequence @angular/core
groups @angular/core

Macro-functions and macro-static methods

The StaticReflector also support calling macros which are functions or static methods that return an expression.

For example, consider the function,

export function wrapInArray<T>(value: T): T[] {
  return [value];
}

wrapInArray can be called in an annotation because it only returns a value and the expression returned conforms to the supported JavaScript subset. An example of this might be

@NgModule({
  declarations: wrapInArray(TypicalComponent)
})
class TypicalModule {}

This is treated as if it was identical to:

@NgModule({
  declarations: [TypicalComponent]
})
class TypicalModule {}

For example, the Router module exports macro static methods that help declare root and child routes.

The collector is simplistic in its determination of what qualifies as a macro function; it can only contain a single return statement.

@basst314
Copy link

thanks @chuckjaz i found this very useful!

@GiuseppePiscopo
Copy link

  • Parenthisis -> Parenthesis

@e-cloud
Copy link

e-cloud commented Jul 11, 2017

@chuckjaz I wonder why RegExp or it literal version is not supported?

export const API_REGEXP = new RegExp('^' + apiPrefix)

export const Default_Interceptor_config: HttpInterceptorConfig = {
    interceptors: [
        {
            interceptor: RequestHeadersInterceptor,
            patterns: [API_REGEXP],   // --> empty array after aot
        },
        {
            interceptor: RequestRestifyInterceptor,
            patterns: [API_REGEXP],   // --> empty array after aot
        },
        {
            interceptor: ResponseErrorForwardInterceptor,
            patterns: [API_REGEXP],   // --> empty array after aot
        },
    ]
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment