diff --git a/angular/common/index.ts b/angular/common/index.ts new file mode 100644 index 0000000..e727e2e --- /dev/null +++ b/angular/common/index.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// This file is not used to build this module. It is only used during editing +// by the TypeScript language service and during build for verification. `ngc` +// replaces this file with production index.ts when it rewrites private symbol +// names. + +export * from './public_api'; diff --git a/angular/common/public_api.ts b/angular/common/public_api.ts new file mode 100644 index 0000000..a55ca93 --- /dev/null +++ b/angular/common/public_api.ts @@ -0,0 +1,16 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * @module + * @description + * Entry point for all public APIs of the common package. + */ +export * from './src/common'; + +// This file only reexports content of the `src` folder. Keep it that way. diff --git a/angular/common/src/common.ts b/angular/common/src/common.ts new file mode 100644 index 0000000..5e79a49 --- /dev/null +++ b/angular/common/src/common.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * @module + * @description + * Entry point for all public APIs of the common package. + */ +export * from './location/index'; +export {NgLocaleLocalization, NgLocalization} from './localization'; +export {parseCookieValue as ɵparseCookieValue} from './cookie'; +export {CommonModule, DeprecatedI18NPipesModule} from './common_module'; +export {NgClass, NgFor, NgForOf, NgForOfContext, NgIf, NgIfContext, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet, NgComponentOutlet} from './directives/index'; +export {DOCUMENT} from './dom_tokens'; +export {AsyncPipe, DatePipe, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCasePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe, UpperCasePipe, TitleCasePipe} from './pipes/index'; +export {PLATFORM_BROWSER_ID as ɵPLATFORM_BROWSER_ID, PLATFORM_SERVER_ID as ɵPLATFORM_SERVER_ID, PLATFORM_WORKER_APP_ID as ɵPLATFORM_WORKER_APP_ID, PLATFORM_WORKER_UI_ID as ɵPLATFORM_WORKER_UI_ID, isPlatformBrowser, isPlatformServer, isPlatformWorkerApp, isPlatformWorkerUi} from './platform_id'; +export {VERSION} from './version'; diff --git a/angular/common/src/common_module.ts b/angular/common/src/common_module.ts new file mode 100644 index 0000000..4a75bb4 --- /dev/null +++ b/angular/common/src/common_module.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {NgModule} from '@angular/core'; + +import {COMMON_DEPRECATED_DIRECTIVES, COMMON_DIRECTIVES} from './directives/index'; +import {NgLocaleLocalization, NgLocalization} from './localization'; +import {COMMON_PIPES} from './pipes/index'; + + +// Note: This does not contain the location providers, +// as they need some platform specific implementations to work. +/** + * The module that includes all the basic Angular directives like {@link NgIf}, {@link NgForOf}, ... + * + * @stable + */ +@NgModule({ + declarations: [COMMON_DIRECTIVES, COMMON_PIPES], + exports: [COMMON_DIRECTIVES, COMMON_PIPES], + providers: [ + {provide: NgLocalization, useClass: NgLocaleLocalization}, + ], +}) +export class CommonModule { +} + +/** + * I18N pipes are being changed to move away from using the JS Intl API. + * + * The former pipes relying on the Intl API will be moved to this module while the `CommonModule` + * will contain the new pipes that do not rely on Intl. + * + * As a first step this module is created empty to ease the migration. + * + * see https://github.com/angular/angular/pull/18284 + * + * @deprecated from v5 + */ +@NgModule({declarations: [], exports: []}) +export class DeprecatedI18NPipesModule { +} diff --git a/angular/common/src/cookie.ts b/angular/common/src/cookie.ts new file mode 100644 index 0000000..1e577cf --- /dev/null +++ b/angular/common/src/cookie.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export function parseCookieValue(cookieStr: string, name: string): string|null { + name = encodeURIComponent(name); + for (const cookie of cookieStr.split(';')) { + const eqIndex = cookie.indexOf('='); + const [cookieName, cookieValue]: string[] = + eqIndex == -1 ? [cookie, ''] : [cookie.slice(0, eqIndex), cookie.slice(eqIndex + 1)]; + if (cookieName.trim() === name) { + return decodeURIComponent(cookieValue); + } + } + return null; +} diff --git a/angular/common/src/directives/index.ts b/angular/common/src/directives/index.ts new file mode 100644 index 0000000..b78d576 --- /dev/null +++ b/angular/common/src/directives/index.ts @@ -0,0 +1,60 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Provider} from '@angular/core'; + +import {NgClass} from './ng_class'; +import {NgComponentOutlet} from './ng_component_outlet'; +import {NgFor, NgForOf, NgForOfContext} from './ng_for_of'; +import {NgIf, NgIfContext} from './ng_if'; +import {NgPlural, NgPluralCase} from './ng_plural'; +import {NgStyle} from './ng_style'; +import {NgSwitch, NgSwitchCase, NgSwitchDefault} from './ng_switch'; +import {NgTemplateOutlet} from './ng_template_outlet'; + +export { + NgClass, + NgComponentOutlet, + NgFor, + NgForOf, + NgForOfContext, + NgIf, + NgIfContext, + NgPlural, + NgPluralCase, + NgStyle, + NgSwitch, + NgSwitchCase, + NgSwitchDefault, + NgTemplateOutlet +}; + + + +/** + * A collection of Angular directives that are likely to be used in each and every Angular + * application. + */ +export const COMMON_DIRECTIVES: Provider[] = [ + NgClass, + NgComponentOutlet, + NgForOf, + NgIf, + NgTemplateOutlet, + NgStyle, + NgSwitch, + NgSwitchCase, + NgSwitchDefault, + NgPlural, + NgPluralCase, +]; + +/** + * A collection of deprecated directives that are no longer part of the core module. + */ +export const COMMON_DEPRECATED_DIRECTIVES: Provider[] = [NgFor]; diff --git a/angular/common/src/directives/ng_class.ts b/angular/common/src/directives/ng_class.ts new file mode 100644 index 0000000..73ec957 --- /dev/null +++ b/angular/common/src/directives/ng_class.ts @@ -0,0 +1,142 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Directive, DoCheck, ElementRef, Input, IterableChanges, IterableDiffer, IterableDiffers, KeyValueChanges, KeyValueDiffer, KeyValueDiffers, Renderer, ɵisListLikeIterable as isListLikeIterable, ɵstringify as stringify} from '@angular/core'; + +/** + * @ngModule CommonModule + * + * @whatItDoes Adds and removes CSS classes on an HTML element. + * + * @howToUse + * ``` + * ... + * + * ... + * + * ... + * + * ... + * + * ... + * ``` + * + * @description + * + * The CSS classes are updated as follows, depending on the type of the expression evaluation: + * - `string` - the CSS classes listed in the string (space delimited) are added, + * - `Array` - the CSS classes declared as Array elements are added, + * - `Object` - keys are CSS classes that get added when the expression given in the value + * evaluates to a truthy value, otherwise they are removed. + * + * @stable + */ +@Directive({selector: '[ngClass]'}) +export class NgClass implements DoCheck { + private _iterableDiffer: IterableDiffer|null; + private _keyValueDiffer: KeyValueDiffer|null; + private _initialClasses: string[] = []; + private _rawClass: string[]|Set|{[klass: string]: any}; + + constructor( + private _iterableDiffers: IterableDiffers, private _keyValueDiffers: KeyValueDiffers, + private _ngEl: ElementRef, private _renderer: Renderer) {} + + @Input('class') + set klass(v: string) { + this._applyInitialClasses(true); + this._initialClasses = typeof v === 'string' ? v.split(/\s+/) : []; + this._applyInitialClasses(false); + this._applyClasses(this._rawClass, false); + } + + @Input() + set ngClass(v: string|string[]|Set|{[klass: string]: any}) { + this._cleanupClasses(this._rawClass); + + this._iterableDiffer = null; + this._keyValueDiffer = null; + + this._rawClass = typeof v === 'string' ? v.split(/\s+/) : v; + + if (this._rawClass) { + if (isListLikeIterable(this._rawClass)) { + this._iterableDiffer = this._iterableDiffers.find(this._rawClass).create(); + } else { + this._keyValueDiffer = this._keyValueDiffers.find(this._rawClass).create(); + } + } + } + + ngDoCheck(): void { + if (this._iterableDiffer) { + const iterableChanges = this._iterableDiffer.diff(this._rawClass as string[]); + if (iterableChanges) { + this._applyIterableChanges(iterableChanges); + } + } else if (this._keyValueDiffer) { + const keyValueChanges = this._keyValueDiffer.diff(this._rawClass as{[k: string]: any}); + if (keyValueChanges) { + this._applyKeyValueChanges(keyValueChanges); + } + } + } + + private _cleanupClasses(rawClassVal: string[]|{[klass: string]: any}): void { + this._applyClasses(rawClassVal, true); + this._applyInitialClasses(false); + } + + private _applyKeyValueChanges(changes: KeyValueChanges): void { + changes.forEachAddedItem((record) => this._toggleClass(record.key, record.currentValue)); + changes.forEachChangedItem((record) => this._toggleClass(record.key, record.currentValue)); + changes.forEachRemovedItem((record) => { + if (record.previousValue) { + this._toggleClass(record.key, false); + } + }); + } + + private _applyIterableChanges(changes: IterableChanges): void { + changes.forEachAddedItem((record) => { + if (typeof record.item === 'string') { + this._toggleClass(record.item, true); + } else { + throw new Error( + `NgClass can only toggle CSS classes expressed as strings, got ${stringify(record.item)}`); + } + }); + + changes.forEachRemovedItem((record) => this._toggleClass(record.item, false)); + } + + private _applyInitialClasses(isCleanup: boolean) { + this._initialClasses.forEach(klass => this._toggleClass(klass, !isCleanup)); + } + + private _applyClasses( + rawClassVal: string[]|Set|{[klass: string]: any}, isCleanup: boolean) { + if (rawClassVal) { + if (Array.isArray(rawClassVal) || rawClassVal instanceof Set) { + (rawClassVal).forEach((klass: string) => this._toggleClass(klass, !isCleanup)); + } else { + Object.keys(rawClassVal).forEach(klass => { + if (rawClassVal[klass] != null) this._toggleClass(klass, !isCleanup); + }); + } + } + } + + private _toggleClass(klass: string, enabled: any): void { + klass = klass.trim(); + if (klass) { + klass.split(/\s+/g).forEach( + klass => { this._renderer.setElementClass(this._ngEl.nativeElement, klass, !!enabled); }); + } + } +} diff --git a/angular/common/src/directives/ng_component_outlet.ts b/angular/common/src/directives/ng_component_outlet.ts new file mode 100644 index 0000000..3c018a7 --- /dev/null +++ b/angular/common/src/directives/ng_component_outlet.ts @@ -0,0 +1,112 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ComponentFactoryResolver, ComponentRef, Directive, Injector, Input, NgModuleFactory, NgModuleRef, OnChanges, OnDestroy, SimpleChanges, StaticProvider, Type, ViewContainerRef} from '@angular/core'; + + +/** + * Instantiates a single {@link Component} type and inserts its Host View into current View. + * `NgComponentOutlet` provides a declarative approach for dynamic component creation. + * + * `NgComponentOutlet` requires a component type, if a falsy value is set the view will clear and + * any existing component will get destroyed. + * + * ### Fine tune control + * + * You can control the component creation process by using the following optional attributes: + * + * * `ngComponentOutletInjector`: Optional custom {@link Injector} that will be used as parent for + * the Component. Defaults to the injector of the current view container. + * + * * `ngComponentOutletContent`: Optional list of projectable nodes to insert into the content + * section of the component, if exists. + * + * * `ngComponentOutletNgModuleFactory`: Optional module factory to allow dynamically loading other + * module, then load a component from that module. + * + * ### Syntax + * + * Simple + * ``` + * + * ``` + * + * Customized injector/content + * ``` + * + * + * ``` + * + * Customized ngModuleFactory + * ``` + * + * + * ``` + * ## Example + * + * {@example common/ngComponentOutlet/ts/module.ts region='SimpleExample'} + * + * A more complete example with additional options: + * + * {@example common/ngComponentOutlet/ts/module.ts region='CompleteExample'} + + * A more complete example with ngModuleFactory: + * + * {@example common/ngComponentOutlet/ts/module.ts region='NgModuleFactoryExample'} + * + * @experimental + */ +@Directive({selector: '[ngComponentOutlet]'}) +export class NgComponentOutlet implements OnChanges, OnDestroy { + @Input() ngComponentOutlet: Type; + @Input() ngComponentOutletInjector: Injector; + @Input() ngComponentOutletContent: any[][]; + @Input() ngComponentOutletNgModuleFactory: NgModuleFactory; + + private _componentRef: ComponentRef|null = null; + private _moduleRef: NgModuleRef|null = null; + + constructor(private _viewContainerRef: ViewContainerRef) {} + + ngOnChanges(changes: SimpleChanges) { + this._viewContainerRef.clear(); + this._componentRef = null; + + if (this.ngComponentOutlet) { + const elInjector = this.ngComponentOutletInjector || this._viewContainerRef.parentInjector; + + if (changes['ngComponentOutletNgModuleFactory']) { + if (this._moduleRef) this._moduleRef.destroy(); + + if (this.ngComponentOutletNgModuleFactory) { + const parentModule = elInjector.get(NgModuleRef); + this._moduleRef = this.ngComponentOutletNgModuleFactory.create(parentModule.injector); + } else { + this._moduleRef = null; + } + } + + const componentFactoryResolver = this._moduleRef ? this._moduleRef.componentFactoryResolver : + elInjector.get(ComponentFactoryResolver); + + const componentFactory = + componentFactoryResolver.resolveComponentFactory(this.ngComponentOutlet); + + this._componentRef = this._viewContainerRef.createComponent( + componentFactory, this._viewContainerRef.length, elInjector, + this.ngComponentOutletContent); + } + } + + ngOnDestroy() { + if (this._moduleRef) this._moduleRef.destroy(); + } +} diff --git a/angular/common/src/directives/ng_for_of.ts b/angular/common/src/directives/ng_for_of.ts new file mode 100644 index 0000000..1441567 --- /dev/null +++ b/angular/common/src/directives/ng_for_of.ts @@ -0,0 +1,214 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ChangeDetectorRef, Directive, DoCheck, EmbeddedViewRef, Input, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDiffers, NgIterable, OnChanges, SimpleChanges, TemplateRef, TrackByFunction, ViewContainerRef, forwardRef, isDevMode} from '@angular/core'; + +/** + * @stable + */ +export class NgForOfContext { + constructor( + public $implicit: T, public ngForOf: NgIterable, public index: number, + public count: number) {} + + get first(): boolean { return this.index === 0; } + + get last(): boolean { return this.index === this.count - 1; } + + get even(): boolean { return this.index % 2 === 0; } + + get odd(): boolean { return !this.even; } +} + +/** + * The `NgForOf` directive instantiates a template once per item from an iterable. The context + * for each instantiated template inherits from the outer context with the given loop variable + * set to the current item from the iterable. + * + * ### Local Variables + * + * `NgForOf` provides several exported values that can be aliased to local variables: + * + * - `$implicit: T`: The value of the individual items in the iterable (`ngForOf`). + * - `ngForOf: NgIterable`: The value of the iterable expression. Useful when the expression is + * more complex then a property access, for example when using the async pipe (`userStreams | + * async`). + * - `index: number`: The index of the current item in the iterable. + * - `first: boolean`: True when the item is the first item in the iterable. + * - `last: boolean`: True when the item is the last item in the iterable. + * - `even: boolean`: True when the item has an even index in the iterable. + * - `odd: boolean`: True when the item has an odd index in the iterable. + * + * ``` + *
  • + * {{i}}/{{users.length}}. {{user}} default + *
  • + * ``` + * + * ### Change Propagation + * + * When the contents of the iterator changes, `NgForOf` makes the corresponding changes to the DOM: + * + * * When an item is added, a new instance of the template is added to the DOM. + * * When an item is removed, its template instance is removed from the DOM. + * * When items are reordered, their respective templates are reordered in the DOM. + * * Otherwise, the DOM element for that item will remain the same. + * + * Angular uses object identity to track insertions and deletions within the iterator and reproduce + * those changes in the DOM. This has important implications for animations and any stateful + * controls (such as `` elements which accept user input) that are present. Inserted rows can + * be animated in, deleted rows can be animated out, and unchanged rows retain any unsaved state + * such as user input. + * + * It is possible for the identities of elements in the iterator to change while the data does not. + * This can happen, for example, if the iterator produced from an RPC to the server, and that + * RPC is re-run. Even if the data hasn't changed, the second response will produce objects with + * different identities, and Angular will tear down the entire DOM and rebuild it (as if all old + * elements were deleted and all new elements inserted). This is an expensive operation and should + * be avoided if possible. + * + * To customize the default tracking algorithm, `NgForOf` supports `trackBy` option. + * `trackBy` takes a function which has two arguments: `index` and `item`. + * If `trackBy` is given, Angular tracks changes by the return value of the function. + * + * ### Syntax + * + * - `
  • ...
  • ` + * - `
  • ...
  • ` + * + * With `` element: + * + * ``` + * + *
  • ...
  • + *
    + * ``` + * + * ### Example + * + * See a [live demo](http://plnkr.co/edit/KVuXxDp0qinGDyo307QW?p=preview) for a more detailed + * example. + * + * @stable + */ +@Directive({selector: '[ngFor][ngForOf]'}) +export class NgForOf implements DoCheck, OnChanges { + @Input() ngForOf: NgIterable; + @Input() + set ngForTrackBy(fn: TrackByFunction) { + if (isDevMode() && fn != null && typeof fn !== 'function') { + // TODO(vicb): use a log service once there is a public one available + if (console && console.warn) { + console.warn( + `trackBy must be a function, but received ${JSON.stringify(fn)}. ` + + `See https://angular.io/docs/ts/latest/api/common/index/NgFor-directive.html#!#change-propagation for more information.`); + } + } + this._trackByFn = fn; + } + + get ngForTrackBy(): TrackByFunction { return this._trackByFn; } + + private _differ: IterableDiffer|null = null; + private _trackByFn: TrackByFunction; + + constructor( + private _viewContainer: ViewContainerRef, private _template: TemplateRef>, + private _differs: IterableDiffers) {} + + @Input() + set ngForTemplate(value: TemplateRef>) { + // TODO(TS2.1): make TemplateRef>> once we move to TS v2.1 + // The current type is too restrictive; a template that just uses index, for example, + // should be acceptable. + if (value) { + this._template = value; + } + } + + ngOnChanges(changes: SimpleChanges): void { + if ('ngForOf' in changes) { + // React on ngForOf changes only once all inputs have been initialized + const value = changes['ngForOf'].currentValue; + if (!this._differ && value) { + try { + this._differ = this._differs.find(value).create(this.ngForTrackBy); + } catch (e) { + throw new Error( + `Cannot find a differ supporting object '${value}' of type '${getTypeNameForDebugging(value)}'. NgFor only supports binding to Iterables such as Arrays.`); + } + } + } + } + + ngDoCheck(): void { + if (this._differ) { + const changes = this._differ.diff(this.ngForOf); + if (changes) this._applyChanges(changes); + } + } + + private _applyChanges(changes: IterableChanges) { + const insertTuples: RecordViewTuple[] = []; + changes.forEachOperation( + (item: IterableChangeRecord, adjustedPreviousIndex: number, currentIndex: number) => { + if (item.previousIndex == null) { + const view = this._viewContainer.createEmbeddedView( + this._template, new NgForOfContext(null !, this.ngForOf, -1, -1), currentIndex); + const tuple = new RecordViewTuple(item, view); + insertTuples.push(tuple); + } else if (currentIndex == null) { + this._viewContainer.remove(adjustedPreviousIndex); + } else { + const view = this._viewContainer.get(adjustedPreviousIndex) !; + this._viewContainer.move(view, currentIndex); + const tuple = new RecordViewTuple(item, >>view); + insertTuples.push(tuple); + } + }); + + for (let i = 0; i < insertTuples.length; i++) { + this._perViewChange(insertTuples[i].view, insertTuples[i].record); + } + + for (let i = 0, ilen = this._viewContainer.length; i < ilen; i++) { + const viewRef = >>this._viewContainer.get(i); + viewRef.context.index = i; + viewRef.context.count = ilen; + } + + changes.forEachIdentityChange((record: any) => { + const viewRef = + >>this._viewContainer.get(record.currentIndex); + viewRef.context.$implicit = record.item; + }); + } + + private _perViewChange( + view: EmbeddedViewRef>, record: IterableChangeRecord) { + view.context.$implicit = record.item; + } +} + +class RecordViewTuple { + constructor(public record: any, public view: EmbeddedViewRef>) {} +} + +/** + * @deprecated from v4.0.0 - Use NgForOf instead. + */ +export type NgFor = NgForOf; + +/** + * @deprecated from v4.0.0 - Use NgForOf instead. + */ +export const NgFor = NgForOf; + +export function getTypeNameForDebugging(type: any): string { + return type['name'] || typeof type; +} diff --git a/angular/common/src/directives/ng_if.ts b/angular/common/src/directives/ng_if.ts new file mode 100644 index 0000000..8943fd6 --- /dev/null +++ b/angular/common/src/directives/ng_if.ts @@ -0,0 +1,163 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Directive, EmbeddedViewRef, Input, TemplateRef, ViewContainerRef} from '@angular/core'; + + +/** + * Conditionally includes a template based on the value of an `expression`. + * + * `ngIf` evaluates the `expression` and then renders the `then` or `else` template in its place + * when expression is truthy or falsy respectively. Typically the: + * - `then` template is the inline template of `ngIf` unless bound to a different value. + * - `else` template is blank unless it is bound. + * + * ## Most common usage + * + * The most common usage of the `ngIf` directive is to conditionally show the inline template as + * seen in this example: + * {@example common/ngIf/ts/module.ts region='NgIfSimple'} + * + * ## Showing an alternative template using `else` + * + * If it is necessary to display a template when the `expression` is falsy use the `else` template + * binding as shown. Note that the `else` binding points to a `` labeled `#elseBlock`. + * The template can be defined anywhere in the component view but is typically placed right after + * `ngIf` for readability. + * + * {@example common/ngIf/ts/module.ts region='NgIfElse'} + * + * ## Using non-inlined `then` template + * + * Usually the `then` template is the inlined template of the `ngIf`, but it can be changed using + * a binding (just like `else`). Because `then` and `else` are bindings, the template references can + * change at runtime as shown in this example. + * + * {@example common/ngIf/ts/module.ts region='NgIfThenElse'} + * + * ## Storing conditional result in a variable + * + * A common pattern is that we need to show a set of properties from the same object. If the + * object is undefined, then we have to use the safe-traversal-operator `?.` to guard against + * dereferencing a `null` value. This is especially the case when waiting on async data such as + * when using the `async` pipe as shown in following example: + * + * ``` + * Hello {{ (userStream|async)?.last }}, {{ (userStream|async)?.first }}! + * ``` + * + * There are several inefficiencies in the above example: + * - We create multiple subscriptions on `userStream`. One for each `async` pipe, or two in the + * example above. + * - We cannot display an alternative screen while waiting for the data to arrive asynchronously. + * - We have to use the safe-traversal-operator `?.` to access properties, which is cumbersome. + * - We have to place the `async` pipe in parenthesis. + * + * A better way to do this is to use `ngIf` and store the result of the condition in a local + * variable as shown in the the example below: + * + * {@example common/ngIf/ts/module.ts region='NgIfAs'} + * + * Notice that: + * - We use only one `async` pipe and hence only one subscription gets created. + * - `ngIf` stores the result of the `userStream|async` in the local variable `user`. + * - The local `user` can then be bound repeatedly in a more efficient way. + * - No need to use the safe-traversal-operator `?.` to access properties as `ngIf` will only + * display the data if `userStream` returns a value. + * - We can display an alternative template while waiting for the data. + * + * ### Syntax + * + * Simple form: + * - `
    ...
    ` + * - `
    ...
    ` + * - `
    ...
    ` + * + * Form with an else block: + * ``` + *
    ...
    + * ... + * ``` + * + * Form with a `then` and `else` block: + * ``` + *
    + * ... + * ... + * ``` + * + * Form with storing the value locally: + * ``` + *
    {{value}}
    + * ... + * ``` + * + * @stable + */ +@Directive({selector: '[ngIf]'}) +export class NgIf { + private _context: NgIfContext = new NgIfContext(); + private _thenTemplateRef: TemplateRef|null = null; + private _elseTemplateRef: TemplateRef|null = null; + private _thenViewRef: EmbeddedViewRef|null = null; + private _elseViewRef: EmbeddedViewRef|null = null; + + constructor(private _viewContainer: ViewContainerRef, templateRef: TemplateRef) { + this._thenTemplateRef = templateRef; + } + + @Input() + set ngIf(condition: any) { + this._context.$implicit = this._context.ngIf = condition; + this._updateView(); + } + + @Input() + set ngIfThen(templateRef: TemplateRef) { + this._thenTemplateRef = templateRef; + this._thenViewRef = null; // clear previous view if any. + this._updateView(); + } + + @Input() + set ngIfElse(templateRef: TemplateRef) { + this._elseTemplateRef = templateRef; + this._elseViewRef = null; // clear previous view if any. + this._updateView(); + } + + private _updateView() { + if (this._context.$implicit) { + if (!this._thenViewRef) { + this._viewContainer.clear(); + this._elseViewRef = null; + if (this._thenTemplateRef) { + this._thenViewRef = + this._viewContainer.createEmbeddedView(this._thenTemplateRef, this._context); + } + } + } else { + if (!this._elseViewRef) { + this._viewContainer.clear(); + this._thenViewRef = null; + if (this._elseTemplateRef) { + this._elseViewRef = + this._viewContainer.createEmbeddedView(this._elseTemplateRef, this._context); + } + } + } + } +} + +/** + * @stable + */ +export class NgIfContext { + public $implicit: any = null; + public ngIf: any = null; +} diff --git a/angular/common/src/directives/ng_plural.ts b/angular/common/src/directives/ng_plural.ts new file mode 100644 index 0000000..e014a5e --- /dev/null +++ b/angular/common/src/directives/ng_plural.ts @@ -0,0 +1,109 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Attribute, Directive, Host, Input, TemplateRef, ViewContainerRef} from '@angular/core'; + +import {NgLocalization, getPluralCategory} from '../localization'; + +import {SwitchView} from './ng_switch'; + + +/** + * @ngModule CommonModule + * + * @whatItDoes Adds / removes DOM sub-trees based on a numeric value. Tailored for pluralization. + * + * @howToUse + * ``` + * + * there is nothing + * there is one + * there are a few + * + * ``` + * + * @description + * + * Displays DOM sub-trees that match the switch expression value, or failing that, DOM sub-trees + * that match the switch expression's pluralization category. + * + * To use this directive you must provide a container element that sets the `[ngPlural]` attribute + * to a switch expression. Inner elements with a `[ngPluralCase]` will display based on their + * expression: + * - if `[ngPluralCase]` is set to a value starting with `=`, it will only display if the value + * matches the switch expression exactly, + * - otherwise, the view will be treated as a "category match", and will only display if exact + * value matches aren't found and the value maps to its category for the defined locale. + * + * See http://cldr.unicode.org/index/cldr-spec/plural-rules + * + * @experimental + */ +@Directive({selector: '[ngPlural]'}) +export class NgPlural { + private _switchValue: number; + private _activeView: SwitchView; + private _caseViews: {[k: string]: SwitchView} = {}; + + constructor(private _localization: NgLocalization) {} + + @Input() + set ngPlural(value: number) { + this._switchValue = value; + this._updateView(); + } + + addCase(value: string, switchView: SwitchView): void { this._caseViews[value] = switchView; } + + private _updateView(): void { + this._clearViews(); + + const cases = Object.keys(this._caseViews); + const key = getPluralCategory(this._switchValue, cases, this._localization); + this._activateView(this._caseViews[key]); + } + + private _clearViews() { + if (this._activeView) this._activeView.destroy(); + } + + private _activateView(view: SwitchView) { + if (view) { + this._activeView = view; + this._activeView.create(); + } + } +} + +/** + * @ngModule CommonModule + * + * @whatItDoes Creates a view that will be added/removed from the parent {@link NgPlural} when the + * given expression matches the plural expression according to CLDR rules. + * + * @howToUse + * ``` + * + * ... + * ... + * + *``` + * + * See {@link NgPlural} for more details and example. + * + * @experimental + */ +@Directive({selector: '[ngPluralCase]'}) +export class NgPluralCase { + constructor( + @Attribute('ngPluralCase') public value: string, template: TemplateRef, + viewContainer: ViewContainerRef, @Host() ngPlural: NgPlural) { + const isANumber: boolean = !isNaN(Number(value)); + ngPlural.addCase(isANumber ? `=${value}` : value, new SwitchView(viewContainer, template)); + } +} diff --git a/angular/common/src/directives/ng_style.ts b/angular/common/src/directives/ng_style.ts new file mode 100644 index 0000000..524f482 --- /dev/null +++ b/angular/common/src/directives/ng_style.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Directive, DoCheck, ElementRef, Input, KeyValueChanges, KeyValueDiffer, KeyValueDiffers, Renderer} from '@angular/core'; + +/** + * @ngModule CommonModule + * + * @whatItDoes Update an HTML element styles. + * + * @howToUse + * ``` + * ... + * + * ... + * + * ... + * ``` + * + * @description + * + * The styles are updated according to the value of the expression evaluation: + * - keys are style names with an optional `.` suffix (ie 'top.px', 'font-style.em'), + * - values are the values assigned to those properties (expressed in the given unit). + * + * @stable + */ +@Directive({selector: '[ngStyle]'}) +export class NgStyle implements DoCheck { + private _ngStyle: {[key: string]: string}; + private _differ: KeyValueDiffer; + + constructor( + private _differs: KeyValueDiffers, private _ngEl: ElementRef, private _renderer: Renderer) {} + + @Input() + set ngStyle(v: {[key: string]: string}) { + this._ngStyle = v; + if (!this._differ && v) { + this._differ = this._differs.find(v).create(); + } + } + + ngDoCheck() { + if (this._differ) { + const changes = this._differ.diff(this._ngStyle); + if (changes) { + this._applyChanges(changes); + } + } + } + + private _applyChanges(changes: KeyValueChanges): void { + changes.forEachRemovedItem((record) => this._setStyle(record.key, null)); + changes.forEachAddedItem((record) => this._setStyle(record.key, record.currentValue)); + changes.forEachChangedItem((record) => this._setStyle(record.key, record.currentValue)); + } + + private _setStyle(nameAndUnit: string, value: string|number|null|undefined): void { + const [name, unit] = nameAndUnit.split('.'); + value = value != null && unit ? `${value}${unit}` : value; + + this._renderer.setElementStyle(this._ngEl.nativeElement, name, value as string); + } +} diff --git a/angular/common/src/directives/ng_switch.ts b/angular/common/src/directives/ng_switch.ts new file mode 100644 index 0000000..1e7f10d --- /dev/null +++ b/angular/common/src/directives/ng_switch.ts @@ -0,0 +1,200 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Directive, DoCheck, Host, Input, TemplateRef, ViewContainerRef} from '@angular/core'; + +export class SwitchView { + private _created = false; + + constructor( + private _viewContainerRef: ViewContainerRef, private _templateRef: TemplateRef) {} + + create(): void { + this._created = true; + this._viewContainerRef.createEmbeddedView(this._templateRef); + } + + destroy(): void { + this._created = false; + this._viewContainerRef.clear(); + } + + enforceState(created: boolean) { + if (created && !this._created) { + this.create(); + } else if (!created && this._created) { + this.destroy(); + } + } +} + +/** + * @ngModule CommonModule + * + * @whatItDoes Adds / removes DOM sub-trees when the nest match expressions matches the switch + * expression. + * + * @howToUse + * ``` + * + * ... + * ... + * ... + * + * + * + * + * + * ... + * + * ``` + * @description + * + * `NgSwitch` stamps out nested views when their match expression value matches the value of the + * switch expression. + * + * In other words: + * - you define a container element (where you place the directive with a switch expression on the + * `[ngSwitch]="..."` attribute) + * - you define inner views inside the `NgSwitch` and place a `*ngSwitchCase` attribute on the view + * root elements. + * + * Elements within `NgSwitch` but outside of a `NgSwitchCase` or `NgSwitchDefault` directives will + * be preserved at the location. + * + * The `ngSwitchCase` directive informs the parent `NgSwitch` of which view to display when the + * expression is evaluated. + * When no matching expression is found on a `ngSwitchCase` view, the `ngSwitchDefault` view is + * stamped out. + * + * @stable + */ +@Directive({selector: '[ngSwitch]'}) +export class NgSwitch { + private _defaultViews: SwitchView[]; + private _defaultUsed = false; + private _caseCount = 0; + private _lastCaseCheckIndex = 0; + private _lastCasesMatched = false; + private _ngSwitch: any; + + @Input() + set ngSwitch(newValue: any) { + this._ngSwitch = newValue; + if (this._caseCount === 0) { + this._updateDefaultCases(true); + } + } + + /** @internal */ + _addCase(): number { return this._caseCount++; } + + /** @internal */ + _addDefault(view: SwitchView) { + if (!this._defaultViews) { + this._defaultViews = []; + } + this._defaultViews.push(view); + } + + /** @internal */ + _matchCase(value: any): boolean { + const matched = value == this._ngSwitch; + this._lastCasesMatched = this._lastCasesMatched || matched; + this._lastCaseCheckIndex++; + if (this._lastCaseCheckIndex === this._caseCount) { + this._updateDefaultCases(!this._lastCasesMatched); + this._lastCaseCheckIndex = 0; + this._lastCasesMatched = false; + } + return matched; + } + + private _updateDefaultCases(useDefault: boolean) { + if (this._defaultViews && useDefault !== this._defaultUsed) { + this._defaultUsed = useDefault; + for (let i = 0; i < this._defaultViews.length; i++) { + const defaultView = this._defaultViews[i]; + defaultView.enforceState(useDefault); + } + } + } +} + +/** + * @ngModule CommonModule + * + * @whatItDoes Creates a view that will be added/removed from the parent {@link NgSwitch} when the + * given expression evaluate to respectively the same/different value as the switch + * expression. + * + * @howToUse + * ``` + * + * ... + * + *``` + * @description + * + * Insert the sub-tree when the expression evaluates to the same value as the enclosing switch + * expression. + * + * If multiple match expressions match the switch expression value, all of them are displayed. + * + * See {@link NgSwitch} for more details and example. + * + * @stable + */ +@Directive({selector: '[ngSwitchCase]'}) +export class NgSwitchCase implements DoCheck { + private _view: SwitchView; + + @Input() + ngSwitchCase: any; + + constructor( + viewContainer: ViewContainerRef, templateRef: TemplateRef, + @Host() private ngSwitch: NgSwitch) { + ngSwitch._addCase(); + this._view = new SwitchView(viewContainer, templateRef); + } + + ngDoCheck() { this._view.enforceState(this.ngSwitch._matchCase(this.ngSwitchCase)); } +} + +/** + * @ngModule CommonModule + * @whatItDoes Creates a view that is added to the parent {@link NgSwitch} when no case expressions + * match the + * switch expression. + * + * @howToUse + * ``` + * + * ... + * ... + * + * ``` + * + * @description + * + * Insert the sub-tree when no case expressions evaluate to the same value as the enclosing switch + * expression. + * + * See {@link NgSwitch} for more details and example. + * + * @stable + */ +@Directive({selector: '[ngSwitchDefault]'}) +export class NgSwitchDefault { + constructor( + viewContainer: ViewContainerRef, templateRef: TemplateRef, + @Host() ngSwitch: NgSwitch) { + ngSwitch._addDefault(new SwitchView(viewContainer, templateRef)); + } +} diff --git a/angular/common/src/directives/ng_template_outlet.ts b/angular/common/src/directives/ng_template_outlet.ts new file mode 100644 index 0000000..5912f9c --- /dev/null +++ b/angular/common/src/directives/ng_template_outlet.ts @@ -0,0 +1,106 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChange, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core'; + +/** + * @ngModule CommonModule + * + * @whatItDoes Inserts an embedded view from a prepared `TemplateRef` + * + * @howToUse + * ``` + * + * ``` + * + * @description + * + * You can attach a context object to the `EmbeddedViewRef` by setting `[ngTemplateOutletContext]`. + * `[ngTemplateOutletContext]` should be an object, the object's keys will be available for binding + * by the local template `let` declarations. + * + * Note: using the key `$implicit` in the context object will set it's value as default. + * + * ## Example + * + * {@example common/ngTemplateOutlet/ts/module.ts region='NgTemplateOutlet'} + * + * @stable + */ +@Directive({selector: '[ngTemplateOutlet]'}) +export class NgTemplateOutlet implements OnChanges { + private _viewRef: EmbeddedViewRef; + + @Input() public ngTemplateOutletContext: Object; + + @Input() public ngTemplateOutlet: TemplateRef; + + constructor(private _viewContainerRef: ViewContainerRef) {} + + /** + * @deprecated v4.0.0 - Renamed to ngTemplateOutletContext. + */ + @Input() + set ngOutletContext(context: Object) { this.ngTemplateOutletContext = context; } + + ngOnChanges(changes: SimpleChanges) { + const recreateView = this._shouldRecreateView(changes); + + if (recreateView) { + if (this._viewRef) { + this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._viewRef)); + } + + if (this.ngTemplateOutlet) { + this._viewRef = this._viewContainerRef.createEmbeddedView( + this.ngTemplateOutlet, this.ngTemplateOutletContext); + } + } else { + if (this._viewRef && this.ngTemplateOutletContext) { + this._updateExistingContext(this.ngTemplateOutletContext); + } + } + } + + /** + * We need to re-create existing embedded view if: + * - templateRef has changed + * - context has changes + * + * We mark context object as changed when the corresponding object + * shape changes (new properties are added or existing properties are removed). + * In other words we consider context with the same properties as "the same" even + * if object reference changes (see https://github.com/angular/angular/issues/13407). + */ + private _shouldRecreateView(changes: SimpleChanges): boolean { + const ctxChange = changes['ngTemplateOutletContext']; + return !!changes['ngTemplateOutlet'] || (ctxChange && this._hasContextShapeChanged(ctxChange)); + } + + private _hasContextShapeChanged(ctxChange: SimpleChange): boolean { + const prevCtxKeys = Object.keys(ctxChange.previousValue || {}); + const currCtxKeys = Object.keys(ctxChange.currentValue || {}); + + if (prevCtxKeys.length === currCtxKeys.length) { + for (let propName of currCtxKeys) { + if (prevCtxKeys.indexOf(propName) === -1) { + return true; + } + } + return false; + } else { + return true; + } + } + + private _updateExistingContext(ctx: Object): void { + for (let propName of Object.keys(ctx)) { + (this._viewRef.context)[propName] = (this.ngTemplateOutletContext)[propName]; + } + } +} diff --git a/angular/common/src/dom_tokens.ts b/angular/common/src/dom_tokens.ts new file mode 100644 index 0000000..c31db44 --- /dev/null +++ b/angular/common/src/dom_tokens.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {InjectionToken} from '@angular/core'; + +/** + * A DI Token representing the main rendering context. In a browser this is the DOM Document. + * + * Note: Document might not be available in the Application Context when Application and Rendering + * Contexts are not the same (e.g. when running the application into a Web Worker). + * + * @stable + */ +export const DOCUMENT = new InjectionToken('DocumentToken'); diff --git a/angular/common/src/localization.ts b/angular/common/src/localization.ts new file mode 100644 index 0000000..c2deb74 --- /dev/null +++ b/angular/common/src/localization.ts @@ -0,0 +1,401 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Inject, Injectable, LOCALE_ID} from '@angular/core'; + +/** + * @experimental + */ +export abstract class NgLocalization { abstract getPluralCategory(value: any): string; } + + +/** + * Returns the plural category for a given value. + * - "=value" when the case exists, + * - the plural category otherwise + * + * @internal + */ +export function getPluralCategory( + value: number, cases: string[], ngLocalization: NgLocalization): string { + let key = `=${value}`; + + if (cases.indexOf(key) > -1) { + return key; + } + + key = ngLocalization.getPluralCategory(value); + + if (cases.indexOf(key) > -1) { + return key; + } + + if (cases.indexOf('other') > -1) { + return 'other'; + } + + throw new Error(`No plural message found for value "${value}"`); +} + +/** + * Returns the plural case based on the locale + * + * @experimental + */ +@Injectable() +export class NgLocaleLocalization extends NgLocalization { + constructor(@Inject(LOCALE_ID) protected locale: string) { super(); } + + getPluralCategory(value: any): string { + const plural = getPluralCase(this.locale, value); + + switch (plural) { + case Plural.Zero: + return 'zero'; + case Plural.One: + return 'one'; + case Plural.Two: + return 'two'; + case Plural.Few: + return 'few'; + case Plural.Many: + return 'many'; + default: + return 'other'; + } + } +} + +// This is generated code DO NOT MODIFY +// see angular/script/cldr/gen_plural_rules.js + +/** @experimental */ +export enum Plural { + Zero, + One, + Two, + Few, + Many, + Other, +} + +/** + * Returns the plural case based on the locale + * + * @experimental + */ +export function getPluralCase(locale: string, nLike: number | string): Plural { + // TODO(vicb): lazy compute + if (typeof nLike === 'string') { + nLike = parseInt(nLike, 10); + } + const n: number = nLike as number; + const nDecimal = n.toString().replace(/^[^.]*\.?/, ''); + const i = Math.floor(Math.abs(n)); + const v = nDecimal.length; + const f = parseInt(nDecimal, 10); + const t = parseInt(n.toString().replace(/^[^.]*\.?|0+$/g, ''), 10) || 0; + + const lang = locale.split('-')[0].toLowerCase(); + + switch (lang) { + case 'af': + case 'asa': + case 'az': + case 'bem': + case 'bez': + case 'bg': + case 'brx': + case 'ce': + case 'cgg': + case 'chr': + case 'ckb': + case 'ee': + case 'el': + case 'eo': + case 'es': + case 'eu': + case 'fo': + case 'fur': + case 'gsw': + case 'ha': + case 'haw': + case 'hu': + case 'jgo': + case 'jmc': + case 'ka': + case 'kk': + case 'kkj': + case 'kl': + case 'ks': + case 'ksb': + case 'ky': + case 'lb': + case 'lg': + case 'mas': + case 'mgo': + case 'ml': + case 'mn': + case 'nb': + case 'nd': + case 'ne': + case 'nn': + case 'nnh': + case 'nyn': + case 'om': + case 'or': + case 'os': + case 'ps': + case 'rm': + case 'rof': + case 'rwk': + case 'saq': + case 'seh': + case 'sn': + case 'so': + case 'sq': + case 'ta': + case 'te': + case 'teo': + case 'tk': + case 'tr': + case 'ug': + case 'uz': + case 'vo': + case 'vun': + case 'wae': + case 'xog': + if (n === 1) return Plural.One; + return Plural.Other; + case 'ak': + case 'ln': + case 'mg': + case 'pa': + case 'ti': + if (n === Math.floor(n) && n >= 0 && n <= 1) return Plural.One; + return Plural.Other; + case 'am': + case 'as': + case 'bn': + case 'fa': + case 'gu': + case 'hi': + case 'kn': + case 'mr': + case 'zu': + if (i === 0 || n === 1) return Plural.One; + return Plural.Other; + case 'ar': + if (n === 0) return Plural.Zero; + if (n === 1) return Plural.One; + if (n === 2) return Plural.Two; + if (n % 100 === Math.floor(n % 100) && n % 100 >= 3 && n % 100 <= 10) return Plural.Few; + if (n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 99) return Plural.Many; + return Plural.Other; + case 'ast': + case 'ca': + case 'de': + case 'en': + case 'et': + case 'fi': + case 'fy': + case 'gl': + case 'it': + case 'nl': + case 'sv': + case 'sw': + case 'ur': + case 'yi': + if (i === 1 && v === 0) return Plural.One; + return Plural.Other; + case 'be': + if (n % 10 === 1 && !(n % 100 === 11)) return Plural.One; + if (n % 10 === Math.floor(n % 10) && n % 10 >= 2 && n % 10 <= 4 && + !(n % 100 >= 12 && n % 100 <= 14)) + return Plural.Few; + if (n % 10 === 0 || n % 10 === Math.floor(n % 10) && n % 10 >= 5 && n % 10 <= 9 || + n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 14) + return Plural.Many; + return Plural.Other; + case 'br': + if (n % 10 === 1 && !(n % 100 === 11 || n % 100 === 71 || n % 100 === 91)) return Plural.One; + if (n % 10 === 2 && !(n % 100 === 12 || n % 100 === 72 || n % 100 === 92)) return Plural.Two; + if (n % 10 === Math.floor(n % 10) && (n % 10 >= 3 && n % 10 <= 4 || n % 10 === 9) && + !(n % 100 >= 10 && n % 100 <= 19 || n % 100 >= 70 && n % 100 <= 79 || + n % 100 >= 90 && n % 100 <= 99)) + return Plural.Few; + if (!(n === 0) && n % 1e6 === 0) return Plural.Many; + return Plural.Other; + case 'bs': + case 'hr': + case 'sr': + if (v === 0 && i % 10 === 1 && !(i % 100 === 11) || f % 10 === 1 && !(f % 100 === 11)) + return Plural.One; + if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 && + !(i % 100 >= 12 && i % 100 <= 14) || + f % 10 === Math.floor(f % 10) && f % 10 >= 2 && f % 10 <= 4 && + !(f % 100 >= 12 && f % 100 <= 14)) + return Plural.Few; + return Plural.Other; + case 'cs': + case 'sk': + if (i === 1 && v === 0) return Plural.One; + if (i === Math.floor(i) && i >= 2 && i <= 4 && v === 0) return Plural.Few; + if (!(v === 0)) return Plural.Many; + return Plural.Other; + case 'cy': + if (n === 0) return Plural.Zero; + if (n === 1) return Plural.One; + if (n === 2) return Plural.Two; + if (n === 3) return Plural.Few; + if (n === 6) return Plural.Many; + return Plural.Other; + case 'da': + if (n === 1 || !(t === 0) && (i === 0 || i === 1)) return Plural.One; + return Plural.Other; + case 'dsb': + case 'hsb': + if (v === 0 && i % 100 === 1 || f % 100 === 1) return Plural.One; + if (v === 0 && i % 100 === 2 || f % 100 === 2) return Plural.Two; + if (v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 3 && i % 100 <= 4 || + f % 100 === Math.floor(f % 100) && f % 100 >= 3 && f % 100 <= 4) + return Plural.Few; + return Plural.Other; + case 'ff': + case 'fr': + case 'hy': + case 'kab': + if (i === 0 || i === 1) return Plural.One; + return Plural.Other; + case 'fil': + if (v === 0 && (i === 1 || i === 2 || i === 3) || + v === 0 && !(i % 10 === 4 || i % 10 === 6 || i % 10 === 9) || + !(v === 0) && !(f % 10 === 4 || f % 10 === 6 || f % 10 === 9)) + return Plural.One; + return Plural.Other; + case 'ga': + if (n === 1) return Plural.One; + if (n === 2) return Plural.Two; + if (n === Math.floor(n) && n >= 3 && n <= 6) return Plural.Few; + if (n === Math.floor(n) && n >= 7 && n <= 10) return Plural.Many; + return Plural.Other; + case 'gd': + if (n === 1 || n === 11) return Plural.One; + if (n === 2 || n === 12) return Plural.Two; + if (n === Math.floor(n) && (n >= 3 && n <= 10 || n >= 13 && n <= 19)) return Plural.Few; + return Plural.Other; + case 'gv': + if (v === 0 && i % 10 === 1) return Plural.One; + if (v === 0 && i % 10 === 2) return Plural.Two; + if (v === 0 && + (i % 100 === 0 || i % 100 === 20 || i % 100 === 40 || i % 100 === 60 || i % 100 === 80)) + return Plural.Few; + if (!(v === 0)) return Plural.Many; + return Plural.Other; + case 'he': + if (i === 1 && v === 0) return Plural.One; + if (i === 2 && v === 0) return Plural.Two; + if (v === 0 && !(n >= 0 && n <= 10) && n % 10 === 0) return Plural.Many; + return Plural.Other; + case 'is': + if (t === 0 && i % 10 === 1 && !(i % 100 === 11) || !(t === 0)) return Plural.One; + return Plural.Other; + case 'ksh': + if (n === 0) return Plural.Zero; + if (n === 1) return Plural.One; + return Plural.Other; + case 'kw': + case 'naq': + case 'se': + case 'smn': + if (n === 1) return Plural.One; + if (n === 2) return Plural.Two; + return Plural.Other; + case 'lag': + if (n === 0) return Plural.Zero; + if ((i === 0 || i === 1) && !(n === 0)) return Plural.One; + return Plural.Other; + case 'lt': + if (n % 10 === 1 && !(n % 100 >= 11 && n % 100 <= 19)) return Plural.One; + if (n % 10 === Math.floor(n % 10) && n % 10 >= 2 && n % 10 <= 9 && + !(n % 100 >= 11 && n % 100 <= 19)) + return Plural.Few; + if (!(f === 0)) return Plural.Many; + return Plural.Other; + case 'lv': + case 'prg': + if (n % 10 === 0 || n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 19 || + v === 2 && f % 100 === Math.floor(f % 100) && f % 100 >= 11 && f % 100 <= 19) + return Plural.Zero; + if (n % 10 === 1 && !(n % 100 === 11) || v === 2 && f % 10 === 1 && !(f % 100 === 11) || + !(v === 2) && f % 10 === 1) + return Plural.One; + return Plural.Other; + case 'mk': + if (v === 0 && i % 10 === 1 || f % 10 === 1) return Plural.One; + return Plural.Other; + case 'mt': + if (n === 1) return Plural.One; + if (n === 0 || n % 100 === Math.floor(n % 100) && n % 100 >= 2 && n % 100 <= 10) + return Plural.Few; + if (n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 19) return Plural.Many; + return Plural.Other; + case 'pl': + if (i === 1 && v === 0) return Plural.One; + if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 && + !(i % 100 >= 12 && i % 100 <= 14)) + return Plural.Few; + if (v === 0 && !(i === 1) && i % 10 === Math.floor(i % 10) && i % 10 >= 0 && i % 10 <= 1 || + v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 5 && i % 10 <= 9 || + v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 12 && i % 100 <= 14) + return Plural.Many; + return Plural.Other; + case 'pt': + if (n === Math.floor(n) && n >= 0 && n <= 2 && !(n === 2)) return Plural.One; + return Plural.Other; + case 'ro': + if (i === 1 && v === 0) return Plural.One; + if (!(v === 0) || n === 0 || + !(n === 1) && n % 100 === Math.floor(n % 100) && n % 100 >= 1 && n % 100 <= 19) + return Plural.Few; + return Plural.Other; + case 'ru': + case 'uk': + if (v === 0 && i % 10 === 1 && !(i % 100 === 11)) return Plural.One; + if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 && + !(i % 100 >= 12 && i % 100 <= 14)) + return Plural.Few; + if (v === 0 && i % 10 === 0 || + v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 5 && i % 10 <= 9 || + v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 11 && i % 100 <= 14) + return Plural.Many; + return Plural.Other; + case 'shi': + if (i === 0 || n === 1) return Plural.One; + if (n === Math.floor(n) && n >= 2 && n <= 10) return Plural.Few; + return Plural.Other; + case 'si': + if (n === 0 || n === 1 || i === 0 && f === 1) return Plural.One; + return Plural.Other; + case 'sl': + if (v === 0 && i % 100 === 1) return Plural.One; + if (v === 0 && i % 100 === 2) return Plural.Two; + if (v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 3 && i % 100 <= 4 || !(v === 0)) + return Plural.Few; + return Plural.Other; + case 'tzm': + if (n === Math.floor(n) && n >= 0 && n <= 1 || n === Math.floor(n) && n >= 11 && n <= 99) + return Plural.One; + return Plural.Other; + // When there is no specification, the default is always "other" + // Spec: http://cldr.unicode.org/index/cldr-spec/plural-rules + // > other (required—general plural form — also used if the language only has a single form) + default: + return Plural.Other; + } +} diff --git a/angular/common/src/location/hash_location_strategy.ts b/angular/common/src/location/hash_location_strategy.ts new file mode 100644 index 0000000..70bd232 --- /dev/null +++ b/angular/common/src/location/hash_location_strategy.ts @@ -0,0 +1,88 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Inject, Injectable, Optional} from '@angular/core'; + + +import {Location} from './location'; +import {APP_BASE_HREF, LocationStrategy} from './location_strategy'; +import {LocationChangeListener, PlatformLocation} from './platform_location'; + + + +/** + * @whatItDoes Use URL hash for storing application location data. + * @description + * `HashLocationStrategy` is a {@link LocationStrategy} used to configure the + * {@link Location} service to represent its state in the + * [hash fragment](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax) + * of the browser's URL. + * + * For instance, if you call `location.go('/foo')`, the browser's URL will become + * `example.com#/foo`. + * + * ### Example + * + * {@example common/location/ts/hash_location_component.ts region='LocationComponent'} + * + * @stable + */ +@Injectable() +export class HashLocationStrategy extends LocationStrategy { + private _baseHref: string = ''; + constructor( + private _platformLocation: PlatformLocation, + @Optional() @Inject(APP_BASE_HREF) _baseHref?: string) { + super(); + if (_baseHref != null) { + this._baseHref = _baseHref; + } + } + + onPopState(fn: LocationChangeListener): void { + this._platformLocation.onPopState(fn); + this._platformLocation.onHashChange(fn); + } + + getBaseHref(): string { return this._baseHref; } + + path(includeHash: boolean = false): string { + // the hash value is always prefixed with a `#` + // and if it is empty then it will stay empty + let path = this._platformLocation.hash; + if (path == null) path = '#'; + + return path.length > 0 ? path.substring(1) : path; + } + + prepareExternalUrl(internal: string): string { + const url = Location.joinWithSlash(this._baseHref, internal); + return url.length > 0 ? ('#' + url) : url; + } + + pushState(state: any, title: string, path: string, queryParams: string) { + let url: string|null = + this.prepareExternalUrl(path + Location.normalizeQueryParams(queryParams)); + if (url.length == 0) { + url = this._platformLocation.pathname; + } + this._platformLocation.pushState(state, title, url); + } + + replaceState(state: any, title: string, path: string, queryParams: string) { + let url = this.prepareExternalUrl(path + Location.normalizeQueryParams(queryParams)); + if (url.length == 0) { + url = this._platformLocation.pathname; + } + this._platformLocation.replaceState(state, title, url); + } + + forward(): void { this._platformLocation.forward(); } + + back(): void { this._platformLocation.back(); } +} diff --git a/angular/common/src/location/index.ts b/angular/common/src/location/index.ts new file mode 100644 index 0000000..5707ccd --- /dev/null +++ b/angular/common/src/location/index.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './platform_location'; +export * from './location_strategy'; +export * from './hash_location_strategy'; +export * from './path_location_strategy'; +export * from './location'; diff --git a/angular/common/src/location/location.ts b/angular/common/src/location/location.ts new file mode 100644 index 0000000..ed89526 --- /dev/null +++ b/angular/common/src/location/location.ts @@ -0,0 +1,189 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {EventEmitter, Injectable} from '@angular/core'; + +import {LocationStrategy} from './location_strategy'; + +/** @experimental */ +export interface PopStateEvent { + pop?: boolean; + type?: string; + url?: string; +} + +/** + * @whatItDoes `Location` is a service that applications can use to interact with a browser's URL. + * @description + * Depending on which {@link LocationStrategy} is used, `Location` will either persist + * to the URL's path or the URL's hash segment. + * + * Note: it's better to use {@link Router#navigate} service to trigger route changes. Use + * `Location` only if you need to interact with or create normalized URLs outside of + * routing. + * + * `Location` is responsible for normalizing the URL against the application's base href. + * A normalized URL is absolute from the URL host, includes the application's base href, and has no + * trailing slash: + * - `/my/app/user/123` is normalized + * - `my/app/user/123` **is not** normalized + * - `/my/app/user/123/` **is not** normalized + * + * ### Example + * {@example common/location/ts/path_location_component.ts region='LocationComponent'} + * @stable + */ +@Injectable() +export class Location { + /** @internal */ + _subject: EventEmitter = new EventEmitter(); + /** @internal */ + _baseHref: string; + /** @internal */ + _platformStrategy: LocationStrategy; + + constructor(platformStrategy: LocationStrategy) { + this._platformStrategy = platformStrategy; + const browserBaseHref = this._platformStrategy.getBaseHref(); + this._baseHref = Location.stripTrailingSlash(_stripIndexHtml(browserBaseHref)); + this._platformStrategy.onPopState((ev) => { + this._subject.emit({ + 'url': this.path(true), + 'pop': true, + 'type': ev.type, + }); + }); + } + + /** + * Returns the normalized URL path. + */ + // TODO: vsavkin. Remove the boolean flag and always include hash once the deprecated router is + // removed. + path(includeHash: boolean = false): string { + return this.normalize(this._platformStrategy.path(includeHash)); + } + + /** + * Normalizes the given path and compares to the current normalized path. + */ + isCurrentPathEqualTo(path: string, query: string = ''): boolean { + return this.path() == this.normalize(path + Location.normalizeQueryParams(query)); + } + + /** + * Given a string representing a URL, returns the normalized URL path without leading or + * trailing slashes. + */ + normalize(url: string): string { + return Location.stripTrailingSlash(_stripBaseHref(this._baseHref, _stripIndexHtml(url))); + } + + /** + * Given a string representing a URL, returns the platform-specific external URL path. + * If the given URL doesn't begin with a leading slash (`'/'`), this method adds one + * before normalizing. This method will also add a hash if `HashLocationStrategy` is + * used, or the `APP_BASE_HREF` if the `PathLocationStrategy` is in use. + */ + prepareExternalUrl(url: string): string { + if (url && url[0] !== '/') { + url = '/' + url; + } + return this._platformStrategy.prepareExternalUrl(url); + } + + // TODO: rename this method to pushState + /** + * Changes the browsers URL to the normalized version of the given URL, and pushes a + * new item onto the platform's history. + */ + go(path: string, query: string = ''): void { + this._platformStrategy.pushState(null, '', path, query); + } + + /** + * Changes the browsers URL to the normalized version of the given URL, and replaces + * the top item on the platform's history stack. + */ + replaceState(path: string, query: string = ''): void { + this._platformStrategy.replaceState(null, '', path, query); + } + + /** + * Navigates forward in the platform's history. + */ + forward(): void { this._platformStrategy.forward(); } + + /** + * Navigates back in the platform's history. + */ + back(): void { this._platformStrategy.back(); } + + /** + * Subscribe to the platform's `popState` events. + */ + subscribe( + onNext: (value: PopStateEvent) => void, onThrow?: ((exception: any) => void)|null, + onReturn?: (() => void)|null): Object { + return this._subject.subscribe({next: onNext, error: onThrow, complete: onReturn}); + } + + /** + * Given a string of url parameters, prepend with '?' if needed, otherwise return parameters as + * is. + */ + public static normalizeQueryParams(params: string): string { + return params && params[0] !== '?' ? '?' + params : params; + } + + /** + * Given 2 parts of a url, join them with a slash if needed. + */ + public static joinWithSlash(start: string, end: string): string { + if (start.length == 0) { + return end; + } + if (end.length == 0) { + return start; + } + let slashes = 0; + if (start.endsWith('/')) { + slashes++; + } + if (end.startsWith('/')) { + slashes++; + } + if (slashes == 2) { + return start + end.substring(1); + } + if (slashes == 1) { + return start + end; + } + return start + '/' + end; + } + + /** + * If url has a trailing slash, remove it, otherwise return url as is. This + * method looks for the first occurence of either #, ?, or the end of the + * line as `/` characters after any of these should not be replaced. + */ + public static stripTrailingSlash(url: string): string { + const match = url.match(/#|\?|$/); + const pathEndIdx = match && match.index || url.length; + const droppedSlashIdx = pathEndIdx - (url[pathEndIdx - 1] === '/' ? 1 : 0); + return url.slice(0, droppedSlashIdx) + url.slice(pathEndIdx); + } +} + +function _stripBaseHref(baseHref: string, url: string): string { + return baseHref && url.startsWith(baseHref) ? url.substring(baseHref.length) : url; +} + +function _stripIndexHtml(url: string): string { + return url.replace(/\/index.html$/, ''); +} diff --git a/angular/common/src/location/location_strategy.ts b/angular/common/src/location/location_strategy.ts new file mode 100644 index 0000000..d5b1997 --- /dev/null +++ b/angular/common/src/location/location_strategy.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {InjectionToken} from '@angular/core'; +import {LocationChangeListener} from './platform_location'; + +/** + * `LocationStrategy` is responsible for representing and reading route state + * from the browser's URL. Angular provides two strategies: + * {@link HashLocationStrategy} and {@link PathLocationStrategy}. + * + * This is used under the hood of the {@link Location} service. + * + * Applications should use the {@link Router} or {@link Location} services to + * interact with application route state. + * + * For instance, {@link HashLocationStrategy} produces URLs like + * `http://example.com#/foo`, and {@link PathLocationStrategy} produces + * `http://example.com/foo` as an equivalent URL. + * + * See these two classes for more. + * + * @stable + */ +export abstract class LocationStrategy { + abstract path(includeHash?: boolean): string; + abstract prepareExternalUrl(internal: string): string; + abstract pushState(state: any, title: string, url: string, queryParams: string): void; + abstract replaceState(state: any, title: string, url: string, queryParams: string): void; + abstract forward(): void; + abstract back(): void; + abstract onPopState(fn: LocationChangeListener): void; + abstract getBaseHref(): string; +} + + +/** + * The `APP_BASE_HREF` token represents the base href to be used with the + * {@link PathLocationStrategy}. + * + * If you're using {@link PathLocationStrategy}, you must provide a provider to a string + * representing the URL prefix that should be preserved when generating and recognizing + * URLs. + * + * ### Example + * + * ```typescript + * import {Component, NgModule} from '@angular/core'; + * import {APP_BASE_HREF} from '@angular/common'; + * + * @NgModule({ + * providers: [{provide: APP_BASE_HREF, useValue: '/my/app'}] + * }) + * class AppModule {} + * ``` + * + * @stable + */ +export const APP_BASE_HREF = new InjectionToken('appBaseHref'); diff --git a/angular/common/src/location/path_location_strategy.ts b/angular/common/src/location/path_location_strategy.ts new file mode 100644 index 0000000..d3ccf7e --- /dev/null +++ b/angular/common/src/location/path_location_strategy.ts @@ -0,0 +1,96 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Inject, Injectable, Optional} from '@angular/core'; + + +import {Location} from './location'; +import {APP_BASE_HREF, LocationStrategy} from './location_strategy'; +import {LocationChangeListener, PlatformLocation} from './platform_location'; + + + +/** + * @whatItDoes Use URL for storing application location data. + * @description + * `PathLocationStrategy` is a {@link LocationStrategy} used to configure the + * {@link Location} service to represent its state in the + * [path](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax) of the + * browser's URL. + * + * If you're using `PathLocationStrategy`, you must provide a {@link APP_BASE_HREF} + * or add a base element to the document. This URL prefix that will be preserved + * when generating and recognizing URLs. + * + * For instance, if you provide an `APP_BASE_HREF` of `'/my/app'` and call + * `location.go('/foo')`, the browser's URL will become + * `example.com/my/app/foo`. + * + * Similarly, if you add `` to the document and call + * `location.go('/foo')`, the browser's URL will become + * `example.com/my/app/foo`. + * + * ### Example + * + * {@example common/location/ts/path_location_component.ts region='LocationComponent'} + * + * @stable + */ +@Injectable() +export class PathLocationStrategy extends LocationStrategy { + private _baseHref: string; + + constructor( + private _platformLocation: PlatformLocation, + @Optional() @Inject(APP_BASE_HREF) href?: string) { + super(); + + if (href == null) { + href = this._platformLocation.getBaseHrefFromDOM(); + } + + if (href == null) { + throw new Error( + `No base href set. Please provide a value for the APP_BASE_HREF token or add a base element to the document.`); + } + + this._baseHref = href; + } + + onPopState(fn: LocationChangeListener): void { + this._platformLocation.onPopState(fn); + this._platformLocation.onHashChange(fn); + } + + getBaseHref(): string { return this._baseHref; } + + prepareExternalUrl(internal: string): string { + return Location.joinWithSlash(this._baseHref, internal); + } + + path(includeHash: boolean = false): string { + const pathname = this._platformLocation.pathname + + Location.normalizeQueryParams(this._platformLocation.search); + const hash = this._platformLocation.hash; + return hash && includeHash ? `${pathname}${hash}` : pathname; + } + + pushState(state: any, title: string, url: string, queryParams: string) { + const externalUrl = this.prepareExternalUrl(url + Location.normalizeQueryParams(queryParams)); + this._platformLocation.pushState(state, title, externalUrl); + } + + replaceState(state: any, title: string, url: string, queryParams: string) { + const externalUrl = this.prepareExternalUrl(url + Location.normalizeQueryParams(queryParams)); + this._platformLocation.replaceState(state, title, externalUrl); + } + + forward(): void { this._platformLocation.forward(); } + + back(): void { this._platformLocation.back(); } +} diff --git a/angular/common/src/location/platform_location.ts b/angular/common/src/location/platform_location.ts new file mode 100644 index 0000000..0f5cb5f --- /dev/null +++ b/angular/common/src/location/platform_location.ts @@ -0,0 +1,66 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {InjectionToken} from '@angular/core'; +/** + * This class should not be used directly by an application developer. Instead, use + * {@link Location}. + * + * `PlatformLocation` encapsulates all calls to DOM apis, which allows the Router to be platform + * agnostic. + * This means that we can have different implementation of `PlatformLocation` for the different + * platforms that angular supports. For example, `@angular/platform-browser` provides an + * implementation specific to the browser environment, while `@angular/platform-webworker` provides + * one suitable for use with web workers. + * + * The `PlatformLocation` class is used directly by all implementations of {@link LocationStrategy} + * when they need to interact with the DOM apis like pushState, popState, etc... + * + * {@link LocationStrategy} in turn is used by the {@link Location} service which is used directly + * by the {@link Router} in order to navigate between routes. Since all interactions between {@link + * Router} / + * {@link Location} / {@link LocationStrategy} and DOM apis flow through the `PlatformLocation` + * class they are all platform independent. + * + * @stable + */ +export abstract class PlatformLocation { + abstract getBaseHrefFromDOM(): string; + abstract onPopState(fn: LocationChangeListener): void; + abstract onHashChange(fn: LocationChangeListener): void; + + abstract get pathname(): string; + abstract get search(): string; + abstract get hash(): string; + + abstract replaceState(state: any, title: string, url: string): void; + + abstract pushState(state: any, title: string, url: string): void; + + abstract forward(): void; + + abstract back(): void; +} + +/** + * @whatItDoes indicates when a location is initialized + * @experimental + */ +export const LOCATION_INITIALIZED = new InjectionToken>('Location Initialized'); + +/** + * A serializable version of the event from onPopState or onHashChange + * + * @experimental + */ +export interface LocationChangeEvent { type: string; } + +/** + * @experimental + */ +export interface LocationChangeListener { (e: LocationChangeEvent): any; } diff --git a/angular/common/src/pipes/async_pipe.ts b/angular/common/src/pipes/async_pipe.ts new file mode 100644 index 0000000..e53de75 --- /dev/null +++ b/angular/common/src/pipes/async_pipe.ts @@ -0,0 +1,145 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ChangeDetectorRef, EventEmitter, OnDestroy, Pipe, PipeTransform, WrappedValue, ɵisObservable, ɵisPromise} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; +import {ISubscription} from 'rxjs/Subscription'; +import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; + +interface SubscriptionStrategy { + createSubscription(async: Observable|Promise, updateLatestValue: any): ISubscription + |Promise; + dispose(subscription: ISubscription|Promise): void; + onDestroy(subscription: ISubscription|Promise): void; +} + +class ObservableStrategy implements SubscriptionStrategy { + createSubscription(async: Observable, updateLatestValue: any): ISubscription { + return async.subscribe({next: updateLatestValue, error: (e: any) => { throw e; }}); + } + + dispose(subscription: ISubscription): void { subscription.unsubscribe(); } + + onDestroy(subscription: ISubscription): void { subscription.unsubscribe(); } +} + +class PromiseStrategy implements SubscriptionStrategy { + createSubscription(async: Promise, updateLatestValue: (v: any) => any): Promise { + return async.then(updateLatestValue, e => { throw e; }); + } + + dispose(subscription: Promise): void {} + + onDestroy(subscription: Promise): void {} +} + +const _promiseStrategy = new PromiseStrategy(); +const _observableStrategy = new ObservableStrategy(); + +/** + * @ngModule CommonModule + * @whatItDoes Unwraps a value from an asynchronous primitive. + * @howToUse `observable_or_promise_expression | async` + * @description + * The `async` pipe subscribes to an `Observable` or `Promise` and returns the latest value it has + * emitted. When a new value is emitted, the `async` pipe marks the component to be checked for + * changes. When the component gets destroyed, the `async` pipe unsubscribes automatically to avoid + * potential memory leaks. + * + * + * ## Examples + * + * This example binds a `Promise` to the view. Clicking the `Resolve` button resolves the + * promise. + * + * {@example common/pipes/ts/async_pipe.ts region='AsyncPipePromise'} + * + * It's also possible to use `async` with Observables. The example below binds the `time` Observable + * to the view. The Observable continuously updates the view with the current time. + * + * {@example common/pipes/ts/async_pipe.ts region='AsyncPipeObservable'} + * + * @stable + */ +@Pipe({name: 'async', pure: false}) +export class AsyncPipe implements OnDestroy, PipeTransform { + private _latestValue: any = null; + private _latestReturnedValue: any = null; + + private _subscription: ISubscription|Promise|null = null; + private _obj: Observable|Promise|EventEmitter|null = null; + private _strategy: SubscriptionStrategy = null !; + + constructor(private _ref: ChangeDetectorRef) {} + + ngOnDestroy(): void { + if (this._subscription) { + this._dispose(); + } + } + + transform(obj: null): null; + transform(obj: undefined): undefined; + transform(obj: Observable): T|null; + transform(obj: Promise): T|null; + transform(obj: Observable|Promise|null|undefined): any { + if (!this._obj) { + if (obj) { + this._subscribe(obj); + } + this._latestReturnedValue = this._latestValue; + return this._latestValue; + } + + if (obj !== this._obj) { + this._dispose(); + return this.transform(obj as any); + } + + if (this._latestValue === this._latestReturnedValue) { + return this._latestReturnedValue; + } + + this._latestReturnedValue = this._latestValue; + return WrappedValue.wrap(this._latestValue); + } + + private _subscribe(obj: Observable|Promise|EventEmitter): void { + this._obj = obj; + this._strategy = this._selectStrategy(obj); + this._subscription = this._strategy.createSubscription( + obj, (value: Object) => this._updateLatestValue(obj, value)); + } + + private _selectStrategy(obj: Observable|Promise|EventEmitter): any { + if (ɵisPromise(obj)) { + return _promiseStrategy; + } + + if (ɵisObservable(obj)) { + return _observableStrategy; + } + + throw invalidPipeArgumentError(AsyncPipe, obj); + } + + private _dispose(): void { + this._strategy.dispose(this._subscription !); + this._latestValue = null; + this._latestReturnedValue = null; + this._subscription = null; + this._obj = null; + } + + private _updateLatestValue(async: any, value: Object): void { + if (async === this._obj) { + this._latestValue = value; + this._ref.markForCheck(); + } + } +} diff --git a/angular/common/src/pipes/case_conversion_pipes.ts b/angular/common/src/pipes/case_conversion_pipes.ts new file mode 100644 index 0000000..59390e4 --- /dev/null +++ b/angular/common/src/pipes/case_conversion_pipes.ts @@ -0,0 +1,72 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Pipe, PipeTransform} from '@angular/core'; +import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; + +/** + * Transforms text to lowercase. + * + * {@example common/pipes/ts/lowerupper_pipe.ts region='LowerUpperPipe' } + * + * @stable + */ +@Pipe({name: 'lowercase'}) +export class LowerCasePipe implements PipeTransform { + transform(value: string): string { + if (!value) return value; + if (typeof value !== 'string') { + throw invalidPipeArgumentError(LowerCasePipe, value); + } + return value.toLowerCase(); + } +} + + +/** + * Helper method to transform a single word to titlecase. + * + * @stable + */ +function titleCaseWord(word: string) { + if (!word) return word; + return word[0].toUpperCase() + word.substr(1).toLowerCase(); +} + +/** + * Transforms text to titlecase. + * + * @stable + */ +@Pipe({name: 'titlecase'}) +export class TitleCasePipe implements PipeTransform { + transform(value: string): string { + if (!value) return value; + if (typeof value !== 'string') { + throw invalidPipeArgumentError(TitleCasePipe, value); + } + + return value.split(/\b/g).map(word => titleCaseWord(word)).join(''); + } +} + +/** + * Transforms text to uppercase. + * + * @stable + */ +@Pipe({name: 'uppercase'}) +export class UpperCasePipe implements PipeTransform { + transform(value: string): string { + if (!value) return value; + if (typeof value !== 'string') { + throw invalidPipeArgumentError(UpperCasePipe, value); + } + return value.toUpperCase(); + } +} diff --git a/angular/common/src/pipes/date_pipe.ts b/angular/common/src/pipes/date_pipe.ts new file mode 100644 index 0000000..aec4cee --- /dev/null +++ b/angular/common/src/pipes/date_pipe.ts @@ -0,0 +1,175 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core'; +import {DateFormatter} from './intl'; +import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; +import {isNumeric} from './number_pipe'; + +const ISO8601_DATE_REGEX = + /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/; +// 1 2 3 4 5 6 7 8 9 10 11 + +/** + * @ngModule CommonModule + * @whatItDoes Formats a date according to locale rules. + * @howToUse `date_expression | date[:format]` + * @description + * + * Where: + * - `expression` is a date object or a number (milliseconds since UTC epoch) or an ISO string + * (https://www.w3.org/TR/NOTE-datetime). + * - `format` indicates which date/time components to include. The format can be predefined as + * shown below or custom as shown in the table. + * - `'medium'`: equivalent to `'yMMMdjms'` (e.g. `Sep 3, 2010, 12:05:08 PM` for `en-US`) + * - `'short'`: equivalent to `'yMdjm'` (e.g. `9/3/2010, 12:05 PM` for `en-US`) + * - `'fullDate'`: equivalent to `'yMMMMEEEEd'` (e.g. `Friday, September 3, 2010` for `en-US`) + * - `'longDate'`: equivalent to `'yMMMMd'` (e.g. `September 3, 2010` for `en-US`) + * - `'mediumDate'`: equivalent to `'yMMMd'` (e.g. `Sep 3, 2010` for `en-US`) + * - `'shortDate'`: equivalent to `'yMd'` (e.g. `9/3/2010` for `en-US`) + * - `'mediumTime'`: equivalent to `'jms'` (e.g. `12:05:08 PM` for `en-US`) + * - `'shortTime'`: equivalent to `'jm'` (e.g. `12:05 PM` for `en-US`) + * + * + * | Component | Symbol | Narrow | Short Form | Long Form | Numeric | 2-digit | + * |-----------|:------:|--------|--------------|-------------------|-----------|-----------| + * | era | G | G (A) | GGG (AD) | GGGG (Anno Domini)| - | - | + * | year | y | - | - | - | y (2015) | yy (15) | + * | month | M | L (S) | MMM (Sep) | MMMM (September) | M (9) | MM (09) | + * | day | d | - | - | - | d (3) | dd (03) | + * | weekday | E | E (S) | EEE (Sun) | EEEE (Sunday) | - | - | + * | hour | j | - | - | - | j (1 PM) | jj (1 PM) | + * | hour12 | h | - | - | - | h (1) | hh (01) | + * | hour24 | H | - | - | - | H (13) | HH (13) | + * | minute | m | - | - | - | m (5) | mm (05) | + * | second | s | - | - | - | s (9) | ss (09) | + * | timezone | z | - | - | z (Pacific Standard Time)| - | - | + * | timezone | Z | - | Z (GMT-8:00) | - | - | - | + * | timezone | a | - | a (PM) | - | - | - | + * + * In javascript, only the components specified will be respected (not the ordering, + * punctuations, ...) and details of the formatting will be dependent on the locale. + * + * Timezone of the formatted text will be the local system timezone of the end-user's machine. + * + * When the expression is a ISO string without time (e.g. 2016-09-19) the time zone offset is not + * applied and the formatted text will have the same day, month and year of the expression. + * + * WARNINGS: + * - this pipe is marked as pure hence it will not be re-evaluated when the input is mutated. + * Instead users should treat the date as an immutable object and change the reference when the + * pipe needs to re-run (this is to avoid reformatting the date on every change detection run + * which would be an expensive operation). + * - this pipe uses the Internationalization API. Therefore it is only reliable in Chrome and Opera + * browsers. + * + * ### Examples + * + * Assuming `dateObj` is (year: 2015, month: 6, day: 15, hour: 21, minute: 43, second: 11) + * in the _local_ time and locale is 'en-US': + * + * ``` + * {{ dateObj | date }} // output is 'Jun 15, 2015' + * {{ dateObj | date:'medium' }} // output is 'Jun 15, 2015, 9:43:11 PM' + * {{ dateObj | date:'shortTime' }} // output is '9:43 PM' + * {{ dateObj | date:'mmss' }} // output is '43:11' + * ``` + * + * {@example common/pipes/ts/date_pipe.ts region='DatePipe'} + * + * @stable + */ +@Pipe({name: 'date', pure: true}) +export class DatePipe implements PipeTransform { + /** @internal */ + static _ALIASES: {[key: string]: string} = { + 'medium': 'yMMMdjms', + 'short': 'yMdjm', + 'fullDate': 'yMMMMEEEEd', + 'longDate': 'yMMMMd', + 'mediumDate': 'yMMMd', + 'shortDate': 'yMd', + 'mediumTime': 'jms', + 'shortTime': 'jm' + }; + + constructor(@Inject(LOCALE_ID) private _locale: string) {} + + transform(value: any, pattern: string = 'mediumDate'): string|null { + let date: Date; + + if (isBlank(value) || value !== value) return null; + + if (typeof value === 'string') { + value = value.trim(); + } + + if (isDate(value)) { + date = value; + } else if (isNumeric(value)) { + date = new Date(parseFloat(value)); + } else if (typeof value === 'string' && /^(\d{4}-\d{1,2}-\d{1,2})$/.test(value)) { + /** + * For ISO Strings without time the day, month and year must be extracted from the ISO String + * before Date creation to avoid time offset and errors in the new Date. + * If we only replace '-' with ',' in the ISO String ("2015,01,01"), and try to create a new + * date, some browsers (e.g. IE 9) will throw an invalid Date error + * If we leave the '-' ("2015-01-01") and try to create a new Date("2015-01-01") the timeoffset + * is applied + * Note: ISO months are 0 for January, 1 for February, ... + */ + const [y, m, d] = value.split('-').map((val: string) => parseInt(val, 10)); + date = new Date(y, m - 1, d); + } else { + date = new Date(value); + } + + if (!isDate(date)) { + let match: RegExpMatchArray|null; + if ((typeof value === 'string') && (match = value.match(ISO8601_DATE_REGEX))) { + date = isoStringToDate(match); + } else { + throw invalidPipeArgumentError(DatePipe, value); + } + } + + return DateFormatter.format(date, this._locale, DatePipe._ALIASES[pattern] || pattern); + } +} + +function isBlank(obj: any): boolean { + return obj == null || obj === ''; +} + +function isDate(obj: any): obj is Date { + return obj instanceof Date && !isNaN(obj.valueOf()); +} + +function isoStringToDate(match: RegExpMatchArray): Date { + const date = new Date(0); + let tzHour = 0; + let tzMin = 0; + const dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear; + const timeSetter = match[8] ? date.setUTCHours : date.setHours; + + if (match[9]) { + tzHour = toInt(match[9] + match[10]); + tzMin = toInt(match[9] + match[11]); + } + dateSetter.call(date, toInt(match[1]), toInt(match[2]) - 1, toInt(match[3])); + const h = toInt(match[4] || '0') - tzHour; + const m = toInt(match[5] || '0') - tzMin; + const s = toInt(match[6] || '0'); + const ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000); + timeSetter.call(date, h, m, s, ms); + return date; +} + +function toInt(str: string): number { + return parseInt(str, 10); +} diff --git a/angular/common/src/pipes/i18n_plural_pipe.ts b/angular/common/src/pipes/i18n_plural_pipe.ts new file mode 100644 index 0000000..dc0909d --- /dev/null +++ b/angular/common/src/pipes/i18n_plural_pipe.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Pipe, PipeTransform} from '@angular/core'; +import {NgLocalization, getPluralCategory} from '../localization'; +import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; + +const _INTERPOLATION_REGEXP: RegExp = /#/g; + +/** + * @ngModule CommonModule + * @whatItDoes Maps a value to a string that pluralizes the value according to locale rules. + * @howToUse `expression | i18nPlural:mapping` + * @description + * + * Where: + * - `expression` is a number. + * - `mapping` is an object that mimics the ICU format, see + * http://userguide.icu-project.org/formatparse/messages + * + * ## Example + * + * {@example common/pipes/ts/i18n_pipe.ts region='I18nPluralPipeComponent'} + * + * @experimental + */ +@Pipe({name: 'i18nPlural', pure: true}) +export class I18nPluralPipe implements PipeTransform { + constructor(private _localization: NgLocalization) {} + + transform(value: number, pluralMap: {[count: string]: string}): string { + if (value == null) return ''; + + if (typeof pluralMap !== 'object' || pluralMap === null) { + throw invalidPipeArgumentError(I18nPluralPipe, pluralMap); + } + + const key = getPluralCategory(value, Object.keys(pluralMap), this._localization); + + return pluralMap[key].replace(_INTERPOLATION_REGEXP, value.toString()); + } +} diff --git a/angular/common/src/pipes/i18n_select_pipe.ts b/angular/common/src/pipes/i18n_select_pipe.ts new file mode 100644 index 0000000..84a44e4 --- /dev/null +++ b/angular/common/src/pipes/i18n_select_pipe.ts @@ -0,0 +1,48 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Pipe, PipeTransform} from '@angular/core'; +import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; + +/** + * @ngModule CommonModule + * @whatItDoes Generic selector that displays the string that matches the current value. + * @howToUse `expression | i18nSelect:mapping` + * @description + * + * Where `mapping` is an object that indicates the text that should be displayed + * for different values of the provided `expression`. + * If none of the keys of the mapping match the value of the `expression`, then the content + * of the `other` key is returned when present, otherwise an empty string is returned. + * + * ## Example + * + * {@example common/pipes/ts/i18n_pipe.ts region='I18nSelectPipeComponent'} + * + * @experimental + */ +@Pipe({name: 'i18nSelect', pure: true}) +export class I18nSelectPipe implements PipeTransform { + transform(value: string|null|undefined, mapping: {[key: string]: string}): string { + if (value == null) return ''; + + if (typeof mapping !== 'object' || typeof value !== 'string') { + throw invalidPipeArgumentError(I18nSelectPipe, mapping); + } + + if (mapping.hasOwnProperty(value)) { + return mapping[value]; + } + + if (mapping.hasOwnProperty('other')) { + return mapping['other']; + } + + return ''; + } +} diff --git a/angular/common/src/pipes/index.ts b/angular/common/src/pipes/index.ts new file mode 100644 index 0000000..7014c62 --- /dev/null +++ b/angular/common/src/pipes/index.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * @module + * @description + * This module provides a set of common Pipes. + */ +import {AsyncPipe} from './async_pipe'; +import {LowerCasePipe, TitleCasePipe, UpperCasePipe} from './case_conversion_pipes'; +import {DatePipe} from './date_pipe'; +import {I18nPluralPipe} from './i18n_plural_pipe'; +import {I18nSelectPipe} from './i18n_select_pipe'; +import {JsonPipe} from './json_pipe'; +import {CurrencyPipe, DecimalPipe, PercentPipe} from './number_pipe'; +import {SlicePipe} from './slice_pipe'; + +export { + AsyncPipe, + CurrencyPipe, + DatePipe, + DecimalPipe, + I18nPluralPipe, + I18nSelectPipe, + JsonPipe, + LowerCasePipe, + PercentPipe, + SlicePipe, + TitleCasePipe, + UpperCasePipe +}; + + +/** + * A collection of Angular pipes that are likely to be used in each and every application. + */ +export const COMMON_PIPES = [ + AsyncPipe, + UpperCasePipe, + LowerCasePipe, + JsonPipe, + SlicePipe, + DecimalPipe, + PercentPipe, + TitleCasePipe, + CurrencyPipe, + DatePipe, + I18nPluralPipe, + I18nSelectPipe, +]; diff --git a/angular/common/src/pipes/intl.ts b/angular/common/src/pipes/intl.ts new file mode 100644 index 0000000..483e89f --- /dev/null +++ b/angular/common/src/pipes/intl.ts @@ -0,0 +1,226 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export enum NumberFormatStyle { + Decimal, + Percent, + Currency, +} + +export class NumberFormatter { + static format(num: number, locale: string, style: NumberFormatStyle, opts: { + minimumIntegerDigits?: number, + minimumFractionDigits?: number, + maximumFractionDigits?: number, + currency?: string|null, + currencyAsSymbol?: boolean + } = {}): string { + const {minimumIntegerDigits, minimumFractionDigits, maximumFractionDigits, currency, + currencyAsSymbol = false} = opts; + const options: Intl.NumberFormatOptions = { + minimumIntegerDigits, + minimumFractionDigits, + maximumFractionDigits, + style: NumberFormatStyle[style].toLowerCase() + }; + + if (style == NumberFormatStyle.Currency) { + options.currency = typeof currency == 'string' ? currency : undefined; + options.currencyDisplay = currencyAsSymbol ? 'symbol' : 'code'; + } + return new Intl.NumberFormat(locale, options).format(num); + } +} + +type DateFormatterFn = (date: Date, locale: string) => string; + +const DATE_FORMATS_SPLIT = + /((?:[^yMLdHhmsazZEwGjJ']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|J+|j+|m+|s+|a|z|Z|G+|w+))(.*)/; + +const PATTERN_ALIASES: {[format: string]: DateFormatterFn} = { + // Keys are quoted so they do not get renamed during closure compilation. + 'yMMMdjms': datePartGetterFactory(combine([ + digitCondition('year', 1), + nameCondition('month', 3), + digitCondition('day', 1), + digitCondition('hour', 1), + digitCondition('minute', 1), + digitCondition('second', 1), + ])), + 'yMdjm': datePartGetterFactory(combine([ + digitCondition('year', 1), digitCondition('month', 1), digitCondition('day', 1), + digitCondition('hour', 1), digitCondition('minute', 1) + ])), + 'yMMMMEEEEd': datePartGetterFactory(combine([ + digitCondition('year', 1), nameCondition('month', 4), nameCondition('weekday', 4), + digitCondition('day', 1) + ])), + 'yMMMMd': datePartGetterFactory( + combine([digitCondition('year', 1), nameCondition('month', 4), digitCondition('day', 1)])), + 'yMMMd': datePartGetterFactory( + combine([digitCondition('year', 1), nameCondition('month', 3), digitCondition('day', 1)])), + 'yMd': datePartGetterFactory( + combine([digitCondition('year', 1), digitCondition('month', 1), digitCondition('day', 1)])), + 'jms': datePartGetterFactory(combine( + [digitCondition('hour', 1), digitCondition('second', 1), digitCondition('minute', 1)])), + 'jm': datePartGetterFactory(combine([digitCondition('hour', 1), digitCondition('minute', 1)])) +}; + +const DATE_FORMATS: {[format: string]: DateFormatterFn} = { + // Keys are quoted so they do not get renamed. + 'yyyy': datePartGetterFactory(digitCondition('year', 4)), + 'yy': datePartGetterFactory(digitCondition('year', 2)), + 'y': datePartGetterFactory(digitCondition('year', 1)), + 'MMMM': datePartGetterFactory(nameCondition('month', 4)), + 'MMM': datePartGetterFactory(nameCondition('month', 3)), + 'MM': datePartGetterFactory(digitCondition('month', 2)), + 'M': datePartGetterFactory(digitCondition('month', 1)), + 'LLLL': datePartGetterFactory(nameCondition('month', 4)), + 'L': datePartGetterFactory(nameCondition('month', 1)), + 'dd': datePartGetterFactory(digitCondition('day', 2)), + 'd': datePartGetterFactory(digitCondition('day', 1)), + 'HH': digitModifier( + hourExtractor(datePartGetterFactory(hour12Modify(digitCondition('hour', 2), false)))), + 'H': hourExtractor(datePartGetterFactory(hour12Modify(digitCondition('hour', 1), false))), + 'hh': digitModifier( + hourExtractor(datePartGetterFactory(hour12Modify(digitCondition('hour', 2), true)))), + 'h': hourExtractor(datePartGetterFactory(hour12Modify(digitCondition('hour', 1), true))), + 'jj': datePartGetterFactory(digitCondition('hour', 2)), + 'j': datePartGetterFactory(digitCondition('hour', 1)), + 'mm': digitModifier(datePartGetterFactory(digitCondition('minute', 2))), + 'm': datePartGetterFactory(digitCondition('minute', 1)), + 'ss': digitModifier(datePartGetterFactory(digitCondition('second', 2))), + 's': datePartGetterFactory(digitCondition('second', 1)), + // while ISO 8601 requires fractions to be prefixed with `.` or `,` + // we can be just safely rely on using `sss` since we currently don't support single or two digit + // fractions + 'sss': datePartGetterFactory(digitCondition('second', 3)), + 'EEEE': datePartGetterFactory(nameCondition('weekday', 4)), + 'EEE': datePartGetterFactory(nameCondition('weekday', 3)), + 'EE': datePartGetterFactory(nameCondition('weekday', 2)), + 'E': datePartGetterFactory(nameCondition('weekday', 1)), + 'a': hourClockExtractor(datePartGetterFactory(hour12Modify(digitCondition('hour', 1), true))), + 'Z': timeZoneGetter('short'), + 'z': timeZoneGetter('long'), + 'ww': datePartGetterFactory({}), // Week of year, padded (00-53). Week 01 is the week with the + // first Thursday of the year. not support ? + 'w': + datePartGetterFactory({}), // Week of year (0-53). Week 1 is the week with the first Thursday + // of the year not support ? + 'G': datePartGetterFactory(nameCondition('era', 1)), + 'GG': datePartGetterFactory(nameCondition('era', 2)), + 'GGG': datePartGetterFactory(nameCondition('era', 3)), + 'GGGG': datePartGetterFactory(nameCondition('era', 4)) +}; + + +function digitModifier(inner: DateFormatterFn): DateFormatterFn { + return function(date: Date, locale: string): string { + const result = inner(date, locale); + return result.length == 1 ? '0' + result : result; + }; +} + +function hourClockExtractor(inner: DateFormatterFn): DateFormatterFn { + return function(date: Date, locale: string): string { return inner(date, locale).split(' ')[1]; }; +} + +function hourExtractor(inner: DateFormatterFn): DateFormatterFn { + return function(date: Date, locale: string): string { return inner(date, locale).split(' ')[0]; }; +} + +function intlDateFormat(date: Date, locale: string, options: Intl.DateTimeFormatOptions): string { + return new Intl.DateTimeFormat(locale, options).format(date).replace(/[\u200e\u200f]/g, ''); +} + +function timeZoneGetter(timezone: string): DateFormatterFn { + // To workaround `Intl` API restriction for single timezone let format with 24 hours + const options = {hour: '2-digit', hour12: false, timeZoneName: timezone}; + return function(date: Date, locale: string): string { + const result = intlDateFormat(date, locale, options); + // Then extract first 3 letters that related to hours + return result ? result.substring(3) : ''; + }; +} + +function hour12Modify( + options: Intl.DateTimeFormatOptions, value: boolean): Intl.DateTimeFormatOptions { + options.hour12 = value; + return options; +} + +function digitCondition(prop: string, len: number): Intl.DateTimeFormatOptions { + const result: {[k: string]: string} = {}; + result[prop] = len === 2 ? '2-digit' : 'numeric'; + return result; +} + +function nameCondition(prop: string, len: number): Intl.DateTimeFormatOptions { + const result: {[k: string]: string} = {}; + if (len < 4) { + result[prop] = len > 1 ? 'short' : 'narrow'; + } else { + result[prop] = 'long'; + } + + return result; +} + +function combine(options: Intl.DateTimeFormatOptions[]): Intl.DateTimeFormatOptions { + return options.reduce((merged, opt) => ({...merged, ...opt}), {}); +} + +function datePartGetterFactory(ret: Intl.DateTimeFormatOptions): DateFormatterFn { + return (date: Date, locale: string): string => intlDateFormat(date, locale, ret); +} + +const DATE_FORMATTER_CACHE = new Map(); + +function dateFormatter(format: string, date: Date, locale: string): string { + const fn = PATTERN_ALIASES[format]; + + if (fn) return fn(date, locale); + + const cacheKey = format; + let parts = DATE_FORMATTER_CACHE.get(cacheKey); + + if (!parts) { + parts = []; + let match: RegExpExecArray|null; + DATE_FORMATS_SPLIT.exec(format); + + let _format: string|null = format; + while (_format) { + match = DATE_FORMATS_SPLIT.exec(_format); + if (match) { + parts = parts.concat(match.slice(1)); + _format = parts.pop() !; + } else { + parts.push(_format); + _format = null; + } + } + + DATE_FORMATTER_CACHE.set(cacheKey, parts); + } + + return parts.reduce((text, part) => { + const fn = DATE_FORMATS[part]; + return text + (fn ? fn(date, locale) : partToTime(part)); + }, ''); +} + +function partToTime(part: string): string { + return part === '\'\'' ? '\'' : part.replace(/(^'|'$)/g, '').replace(/''/g, '\''); +} + +export class DateFormatter { + static format(date: Date, locale: string, pattern: string): string { + return dateFormatter(pattern, date, locale); + } +} diff --git a/angular/common/src/pipes/invalid_pipe_argument_error.ts b/angular/common/src/pipes/invalid_pipe_argument_error.ts new file mode 100644 index 0000000..92add35 --- /dev/null +++ b/angular/common/src/pipes/invalid_pipe_argument_error.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Type, ɵstringify as stringify} from '@angular/core'; + +export function invalidPipeArgumentError(type: Type, value: Object) { + return Error(`InvalidPipeArgument: '${value}' for pipe '${stringify(type)}'`); +} diff --git a/angular/common/src/pipes/json_pipe.ts b/angular/common/src/pipes/json_pipe.ts new file mode 100644 index 0000000..887c830 --- /dev/null +++ b/angular/common/src/pipes/json_pipe.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Pipe, PipeTransform} from '@angular/core'; + +/** + * @ngModule CommonModule + * @whatItDoes Converts value into JSON string. + * @howToUse `expression | json` + * @description + * + * Converts value into string using `JSON.stringify`. Useful for debugging. + * + * ### Example + * {@example common/pipes/ts/json_pipe.ts region='JsonPipe'} + * + * @stable + */ +@Pipe({name: 'json', pure: false}) +export class JsonPipe implements PipeTransform { + transform(value: any): string { return JSON.stringify(value, null, 2); } +} diff --git a/angular/common/src/pipes/number_pipe.ts b/angular/common/src/pipes/number_pipe.ts new file mode 100644 index 0000000..ae170dd --- /dev/null +++ b/angular/common/src/pipes/number_pipe.ts @@ -0,0 +1,174 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Inject, LOCALE_ID, Pipe, PipeTransform, Type} from '@angular/core'; +import {NumberFormatStyle, NumberFormatter} from './intl'; +import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; + +const _NUMBER_FORMAT_REGEXP = /^(\d+)?\.((\d+)(-(\d+))?)?$/; + +function formatNumber( + pipe: Type, locale: string, value: number | string, style: NumberFormatStyle, + digits?: string | null, currency: string | null = null, + currencyAsSymbol: boolean = false): string|null { + if (value == null) return null; + + // Convert strings to numbers + value = typeof value === 'string' && isNumeric(value) ? +value : value; + if (typeof value !== 'number') { + throw invalidPipeArgumentError(pipe, value); + } + + let minInt: number|undefined = undefined; + let minFraction: number|undefined = undefined; + let maxFraction: number|undefined = undefined; + if (style !== NumberFormatStyle.Currency) { + // rely on Intl default for currency + minInt = 1; + minFraction = 0; + maxFraction = 3; + } + + if (digits) { + const parts = digits.match(_NUMBER_FORMAT_REGEXP); + if (parts === null) { + throw new Error(`${digits} is not a valid digit info for number pipes`); + } + if (parts[1] != null) { // min integer digits + minInt = parseIntAutoRadix(parts[1]); + } + if (parts[3] != null) { // min fraction digits + minFraction = parseIntAutoRadix(parts[3]); + } + if (parts[5] != null) { // max fraction digits + maxFraction = parseIntAutoRadix(parts[5]); + } + } + + return NumberFormatter.format(value as number, locale, style, { + minimumIntegerDigits: minInt, + minimumFractionDigits: minFraction, + maximumFractionDigits: maxFraction, + currency: currency, + currencyAsSymbol: currencyAsSymbol, + }); +} + +/** + * @ngModule CommonModule + * @whatItDoes Formats a number according to locale rules. + * @howToUse `number_expression | number[:digitInfo]` + * + * Formats a number as text. Group sizing and separator and other locale-specific + * configurations are based on the active locale. + * + * where `expression` is a number: + * - `digitInfo` is a `string` which has a following format:
    + * {minIntegerDigits}.{minFractionDigits}-{maxFractionDigits} + * - `minIntegerDigits` is the minimum number of integer digits to use. Defaults to `1`. + * - `minFractionDigits` is the minimum number of digits after fraction. Defaults to `0`. + * - `maxFractionDigits` is the maximum number of digits after fraction. Defaults to `3`. + * + * For more information on the acceptable range for each of these numbers and other + * details see your native internationalization library. + * + * WARNING: this pipe uses the Internationalization API which is not yet available in all browsers + * and may require a polyfill. See [Browser Support](guide/browser-support) for details. + * + * ### Example + * + * {@example common/pipes/ts/number_pipe.ts region='NumberPipe'} + * + * @stable + */ +@Pipe({name: 'number'}) +export class DecimalPipe implements PipeTransform { + constructor(@Inject(LOCALE_ID) private _locale: string) {} + + transform(value: any, digits?: string): string|null { + return formatNumber(DecimalPipe, this._locale, value, NumberFormatStyle.Decimal, digits); + } +} + +/** + * @ngModule CommonModule + * @whatItDoes Formats a number as a percentage according to locale rules. + * @howToUse `number_expression | percent[:digitInfo]` + * + * @description + * + * Formats a number as percentage. + * + * - `digitInfo` See {@link DecimalPipe} for detailed description. + * + * WARNING: this pipe uses the Internationalization API which is not yet available in all browsers + * and may require a polyfill. See [Browser Support](guide/browser-support) for details. + * + * ### Example + * + * {@example common/pipes/ts/number_pipe.ts region='PercentPipe'} + * + * @stable + */ +@Pipe({name: 'percent'}) +export class PercentPipe implements PipeTransform { + constructor(@Inject(LOCALE_ID) private _locale: string) {} + + transform(value: any, digits?: string): string|null { + return formatNumber(PercentPipe, this._locale, value, NumberFormatStyle.Percent, digits); + } +} + +/** + * @ngModule CommonModule + * @whatItDoes Formats a number as currency using locale rules. + * @howToUse `number_expression | currency[:currencyCode[:symbolDisplay[:digitInfo]]]` + * @description + * + * Use `currency` to format a number as currency. + * + * - `currencyCode` is the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code, such + * as `USD` for the US dollar and `EUR` for the euro. + * - `symbolDisplay` is a boolean indicating whether to use the currency symbol or code. + * - `true`: use symbol (e.g. `$`). + * - `false`(default): use code (e.g. `USD`). + * - `digitInfo` See {@link DecimalPipe} for detailed description. + * + * WARNING: this pipe uses the Internationalization API which is not yet available in all browsers + * and may require a polyfill. See [Browser Support](guide/browser-support) for details. + * + * ### Example + * + * {@example common/pipes/ts/number_pipe.ts region='CurrencyPipe'} + * + * @stable + */ +@Pipe({name: 'currency'}) +export class CurrencyPipe implements PipeTransform { + constructor(@Inject(LOCALE_ID) private _locale: string) {} + + transform( + value: any, currencyCode: string = 'USD', symbolDisplay: boolean = false, + digits?: string): string|null { + return formatNumber( + CurrencyPipe, this._locale, value, NumberFormatStyle.Currency, digits, currencyCode, + symbolDisplay); + } +} + +function parseIntAutoRadix(text: string): number { + const result: number = parseInt(text); + if (isNaN(result)) { + throw new Error('Invalid integer literal when parsing ' + text); + } + return result; +} + +export function isNumeric(value: any): boolean { + return !isNaN(value - parseFloat(value)); +} diff --git a/angular/common/src/pipes/slice_pipe.ts b/angular/common/src/pipes/slice_pipe.ts new file mode 100644 index 0000000..6466d26 --- /dev/null +++ b/angular/common/src/pipes/slice_pipe.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Pipe, PipeTransform} from '@angular/core'; +import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; + +/** + * @ngModule CommonModule + * @whatItDoes Creates a new List or String containing a subset (slice) of the elements. + * @howToUse `array_or_string_expression | slice:start[:end]` + * @description + * + * Where the input expression is a `List` or `String`, and: + * - `start`: The starting index of the subset to return. + * - **a positive integer**: return the item at `start` index and all items after + * in the list or string expression. + * - **a negative integer**: return the item at `start` index from the end and all items after + * in the list or string expression. + * - **if positive and greater than the size of the expression**: return an empty list or string. + * - **if negative and greater than the size of the expression**: return entire list or string. + * - `end`: The ending index of the subset to return. + * - **omitted**: return all items until the end. + * - **if positive**: return all items before `end` index of the list or string. + * - **if negative**: return all items before `end` index from the end of the list or string. + * + * All behavior is based on the expected behavior of the JavaScript API `Array.prototype.slice()` + * and `String.prototype.slice()`. + * + * When operating on a [List], the returned list is always a copy even when all + * the elements are being returned. + * + * When operating on a blank value, the pipe returns the blank value. + * + * ## List Example + * + * This `ngFor` example: + * + * {@example common/pipes/ts/slice_pipe.ts region='SlicePipe_list'} + * + * produces the following: + * + *
  • b
  • + *
  • c
  • + * + * ## String Examples + * + * {@example common/pipes/ts/slice_pipe.ts region='SlicePipe_string'} + * + * @stable + */ + +@Pipe({name: 'slice', pure: false}) +export class SlicePipe implements PipeTransform { + transform(value: any, start: number, end?: number): any { + if (value == null) return value; + + if (!this.supports(value)) { + throw invalidPipeArgumentError(SlicePipe, value); + } + + return value.slice(start, end); + } + + private supports(obj: any): boolean { return typeof obj === 'string' || Array.isArray(obj); } +} diff --git a/angular/common/src/platform_id.ts b/angular/common/src/platform_id.ts new file mode 100644 index 0000000..2a27926 --- /dev/null +++ b/angular/common/src/platform_id.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export const PLATFORM_BROWSER_ID = 'browser'; +export const PLATFORM_SERVER_ID = 'server'; +export const PLATFORM_WORKER_APP_ID = 'browserWorkerApp'; +export const PLATFORM_WORKER_UI_ID = 'browserWorkerUi'; + +/** + * Returns whether a platform id represents a browser platform. + * @experimental + */ +export function isPlatformBrowser(platformId: Object): boolean { + return platformId === PLATFORM_BROWSER_ID; +} + +/** + * Returns whether a platform id represents a server platform. + * @experimental + */ +export function isPlatformServer(platformId: Object): boolean { + return platformId === PLATFORM_SERVER_ID; +} + +/** + * Returns whether a platform id represents a web worker app platform. + * @experimental + */ +export function isPlatformWorkerApp(platformId: Object): boolean { + return platformId === PLATFORM_WORKER_APP_ID; +} + +/** + * Returns whether a platform id represents a web worker UI platform. + * @experimental + */ +export function isPlatformWorkerUi(platformId: Object): boolean { + return platformId === PLATFORM_WORKER_UI_ID; +} diff --git a/angular/common/src/version.ts b/angular/common/src/version.ts new file mode 100644 index 0000000..19db5be --- /dev/null +++ b/angular/common/src/version.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * @module + * @description + * Entry point for all public APIs of the common package. + */ + +import {Version} from '@angular/core'; +/** + * @stable + */ +export const VERSION = new Version('0.0.0-PLACEHOLDER'); diff --git a/angular/compiler/index.ts b/angular/compiler/index.ts new file mode 100644 index 0000000..03a69b6 --- /dev/null +++ b/angular/compiler/index.ts @@ -0,0 +1,16 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * @module + * @description + * Entry point for all public APIs of the compiler package. + */ +export * from './src/compiler'; + +// This file only reexports content of the `src` folder. Keep it that way. diff --git a/angular/compiler/src/aot/compiler.ts b/angular/compiler/src/aot/compiler.ts new file mode 100644 index 0000000..76d3aca --- /dev/null +++ b/angular/compiler/src/aot/compiler.ts @@ -0,0 +1,578 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileNgModuleSummary, CompilePipeMetadata, CompileProviderMetadata, CompileStylesheetMetadata, CompileSummaryKind, CompileTypeMetadata, CompileTypeSummary, componentFactoryName, createHostComponentMeta, flatten, identifierName, sourceUrl, templateSourceUrl} from '../compile_metadata'; +import {CompilerConfig} from '../config'; +import {Identifiers, createTokenForExternalReference} from '../identifiers'; +import {CompileMetadataResolver} from '../metadata_resolver'; +import {NgModuleCompiler} from '../ng_module_compiler'; +import {OutputEmitter} from '../output/abstract_emitter'; +import * as o from '../output/output_ast'; +import {CompiledStylesheet, StyleCompiler} from '../style_compiler'; +import {SummaryResolver} from '../summary_resolver'; +import {TemplateParser} from '../template_parser/template_parser'; +import {OutputContext, syntaxError} from '../util'; +import {ViewCompileResult, ViewCompiler} from '../view_compiler/view_compiler'; + +import {AotCompilerHost} from './compiler_host'; +import {GeneratedFile} from './generated_file'; +import {StaticReflector} from './static_reflector'; +import {StaticSymbol} from './static_symbol'; +import {ResolvedStaticSymbol, StaticSymbolResolver} from './static_symbol_resolver'; +import {createForJitStub, serializeSummaries} from './summary_serializer'; +import {ngfactoryFilePath, splitTypescriptSuffix, summaryFileName, summaryForJitFileName, summaryForJitName} from './util'; + +export class AotCompiler { + constructor( + private _config: CompilerConfig, private _host: AotCompilerHost, + private _reflector: StaticReflector, private _metadataResolver: CompileMetadataResolver, + private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler, + private _viewCompiler: ViewCompiler, private _ngModuleCompiler: NgModuleCompiler, + private _outputEmitter: OutputEmitter, + private _summaryResolver: SummaryResolver, private _localeId: string|null, + private _translationFormat: string|null, private _enableSummariesForJit: boolean|null, + private _symbolResolver: StaticSymbolResolver) {} + + clearCache() { this._metadataResolver.clearCache(); } + + analyzeModulesSync(rootFiles: string[]): NgAnalyzedModules { + const programSymbols = extractProgramSymbols(this._symbolResolver, rootFiles, this._host); + const analyzeResult = + analyzeAndValidateNgModules(programSymbols, this._host, this._metadataResolver); + analyzeResult.ngModules.forEach( + ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata( + ngModule.type.reference, true)); + return analyzeResult; + } + + analyzeModulesAsync(rootFiles: string[]): Promise { + const programSymbols = extractProgramSymbols(this._symbolResolver, rootFiles, this._host); + const analyzeResult = + analyzeAndValidateNgModules(programSymbols, this._host, this._metadataResolver); + return Promise + .all(analyzeResult.ngModules.map( + ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata( + ngModule.type.reference, false))) + .then(() => analyzeResult); + } + + emitAllStubs(analyzeResult: NgAnalyzedModules): GeneratedFile[] { + const {files} = analyzeResult; + const sourceModules = files.map( + file => + this._compileStubFile(file.srcUrl, file.directives, file.pipes, file.ngModules, false)); + return flatten(sourceModules); + } + + emitPartialStubs(analyzeResult: NgAnalyzedModules): GeneratedFile[] { + const {files} = analyzeResult; + const sourceModules = files.map( + file => + this._compileStubFile(file.srcUrl, file.directives, file.pipes, file.ngModules, true)); + return flatten(sourceModules); + } + + emitAllImpls(analyzeResult: NgAnalyzedModules): GeneratedFile[] { + const {ngModuleByPipeOrDirective, files} = analyzeResult; + const sourceModules = files.map( + file => this._compileImplFile( + file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.pipes, file.ngModules, + file.injectables)); + return flatten(sourceModules); + } + + private _compileStubFile( + srcFileUrl: string, directives: StaticSymbol[], pipes: StaticSymbol[], + ngModules: StaticSymbol[], partial: boolean): GeneratedFile[] { + // partial is true when we only need the files we are certain will produce a factory and/or + // summary. + // This is the normal case for `ngc` but if we assume libraryies are generating their own + // factories + // then we might need a factory for a file that re-exports a module or factory which we cannot + // know + // ahead of time so we need a stub generate for all non-.d.ts files. The .d.ts files do not need + // to + // be excluded here because they are excluded when the modules are analyzed. If a factory ends + // up + // not being needed, the factory file is not written in writeFile callback. + const fileSuffix = splitTypescriptSuffix(srcFileUrl, true)[1]; + const generatedFiles: GeneratedFile[] = []; + + const ngFactoryOutputCtx = this._createOutputContext(ngfactoryFilePath(srcFileUrl, true)); + const jitSummaryOutputCtx = this._createOutputContext(summaryForJitFileName(srcFileUrl, true)); + + // create exports that user code can reference + ngModules.forEach((ngModuleReference) => { + this._ngModuleCompiler.createStub(ngFactoryOutputCtx, ngModuleReference); + createForJitStub(jitSummaryOutputCtx, ngModuleReference); + }); + + let partialJitStubRequired = false; + let partialFactoryStubRequired = false; + + // create stubs for external stylesheets (always empty, as users should not import anything from + // the generated code) + directives.forEach((dirType) => { + const compMeta = this._metadataResolver.getDirectiveMetadata(dirType); + + partialJitStubRequired = true; + + if (!compMeta.isComponent) { + return; + } + // Note: compMeta is a component and therefore template is non null. + compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => { + const styleContext = this._createOutputContext(_stylesModuleUrl( + stylesheetMeta.moduleUrl !, this._styleCompiler.needsStyleShim(compMeta), fileSuffix)); + _createTypeReferenceStub(styleContext, Identifiers.ComponentFactory); + generatedFiles.push(this._codegenSourceModule(stylesheetMeta.moduleUrl !, styleContext)); + }); + + partialFactoryStubRequired = true; + }); + + // If we need all the stubs to be generated then insert an arbitrary reference into the stub + if ((partialFactoryStubRequired || !partial) && ngFactoryOutputCtx.statements.length <= 0) { + _createTypeReferenceStub(ngFactoryOutputCtx, Identifiers.ComponentFactory); + } + if ((partialJitStubRequired || !partial || (pipes && pipes.length > 0)) && + jitSummaryOutputCtx.statements.length <= 0) { + _createTypeReferenceStub(jitSummaryOutputCtx, Identifiers.ComponentFactory); + } + + // Note: we are creating stub ngfactory/ngsummary for all source files, + // as the real calculation requires almost the same logic as producing the real content for + // them. Our pipeline will filter out empty ones at the end. Because of this filter, however, + // stub references to the reference type needs to be generated even if the user cannot + // refer to type from the `.d.ts` file to prevent the file being elided from the emit. + generatedFiles.push(this._codegenSourceModule(srcFileUrl, ngFactoryOutputCtx)); + if (this._enableSummariesForJit) { + generatedFiles.push(this._codegenSourceModule(srcFileUrl, jitSummaryOutputCtx)); + } + + return generatedFiles; + } + + private _compileImplFile( + srcFileUrl: string, ngModuleByPipeOrDirective: Map, + directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: StaticSymbol[], + injectables: StaticSymbol[]): GeneratedFile[] { + const fileSuffix = splitTypescriptSuffix(srcFileUrl, true)[1]; + const generatedFiles: GeneratedFile[] = []; + + const outputCtx = this._createOutputContext(ngfactoryFilePath(srcFileUrl, true)); + + generatedFiles.push( + ...this._createSummary(srcFileUrl, directives, pipes, ngModules, injectables, outputCtx)); + + // compile all ng modules + ngModules.forEach((ngModuleType) => this._compileModule(outputCtx, ngModuleType)); + + // compile components + directives.forEach((dirType) => { + const compMeta = this._metadataResolver.getDirectiveMetadata(dirType); + if (!compMeta.isComponent) { + return; + } + const ngModule = ngModuleByPipeOrDirective.get(dirType); + if (!ngModule) { + throw new Error( + `Internal Error: cannot determine the module for component ${identifierName(compMeta.type)}!`); + } + + // compile styles + const componentStylesheet = this._styleCompiler.compileComponent(outputCtx, compMeta); + // Note: compMeta is a component and therefore template is non null. + compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => { + generatedFiles.push( + this._codegenStyles(stylesheetMeta.moduleUrl !, compMeta, stylesheetMeta, fileSuffix)); + }); + + // compile components + const compViewVars = this._compileComponent( + outputCtx, compMeta, ngModule, ngModule.transitiveModule.directives, componentStylesheet, + fileSuffix); + this._compileComponentFactory(outputCtx, compMeta, ngModule, fileSuffix); + }); + if (outputCtx.statements.length > 0) { + const srcModule = this._codegenSourceModule(srcFileUrl, outputCtx); + generatedFiles.unshift(srcModule); + } + return generatedFiles; + } + + private _createSummary( + srcFileName: string, directives: StaticSymbol[], pipes: StaticSymbol[], + ngModules: StaticSymbol[], injectables: StaticSymbol[], + ngFactoryCtx: OutputContext): GeneratedFile[] { + const symbolSummaries = this._symbolResolver.getSymbolsOf(srcFileName) + .map(symbol => this._symbolResolver.resolveSymbol(symbol)); + const typeData: { + summary: CompileTypeSummary, + metadata: CompileNgModuleMetadata | CompileDirectiveMetadata | CompilePipeMetadata | + CompileTypeMetadata + }[] = + [ + ...ngModules.map(ref => ({ + summary: this._metadataResolver.getNgModuleSummary(ref) !, + metadata: this._metadataResolver.getNgModuleMetadata(ref) ! + })), + ...directives.map(ref => ({ + summary: this._metadataResolver.getDirectiveSummary(ref) !, + metadata: this._metadataResolver.getDirectiveMetadata(ref) ! + })), + ...pipes.map(ref => ({ + summary: this._metadataResolver.getPipeSummary(ref) !, + metadata: this._metadataResolver.getPipeMetadata(ref) ! + })), + ...injectables.map(ref => ({ + summary: this._metadataResolver.getInjectableSummary(ref) !, + metadata: this._metadataResolver.getInjectableSummary(ref) !.type + })) + ]; + const forJitOutputCtx = this._createOutputContext(summaryForJitFileName(srcFileName, true)); + const {json, exportAs} = serializeSummaries( + srcFileName, forJitOutputCtx, this._summaryResolver, this._symbolResolver, symbolSummaries, + typeData); + exportAs.forEach((entry) => { + ngFactoryCtx.statements.push( + o.variable(entry.exportAs).set(ngFactoryCtx.importExpr(entry.symbol)).toDeclStmt(null, [ + o.StmtModifier.Exported + ])); + }); + const summaryJson = new GeneratedFile(srcFileName, summaryFileName(srcFileName), json); + if (this._enableSummariesForJit) { + return [summaryJson, this._codegenSourceModule(srcFileName, forJitOutputCtx)]; + }; + + return [summaryJson]; + } + + private _compileModule(outputCtx: OutputContext, ngModuleType: StaticSymbol): void { + const ngModule = this._metadataResolver.getNgModuleMetadata(ngModuleType) !; + const providers: CompileProviderMetadata[] = []; + + if (this._localeId) { + providers.push({ + token: createTokenForExternalReference(this._reflector, Identifiers.LOCALE_ID), + useValue: this._localeId, + }); + } + + if (this._translationFormat) { + providers.push({ + token: createTokenForExternalReference(this._reflector, Identifiers.TRANSLATIONS_FORMAT), + useValue: this._translationFormat + }); + } + + this._ngModuleCompiler.compile(outputCtx, ngModule, providers); + } + + private _compileComponentFactory( + outputCtx: OutputContext, compMeta: CompileDirectiveMetadata, + ngModule: CompileNgModuleMetadata, fileSuffix: string): void { + const hostType = this._metadataResolver.getHostComponentType(compMeta.type.reference); + const hostMeta = createHostComponentMeta( + hostType, compMeta, this._metadataResolver.getHostComponentViewClass(hostType)); + const hostViewFactoryVar = + this._compileComponent(outputCtx, hostMeta, ngModule, [compMeta.type], null, fileSuffix) + .viewClassVar; + const compFactoryVar = componentFactoryName(compMeta.type.reference); + const inputsExprs: o.LiteralMapEntry[] = []; + for (let propName in compMeta.inputs) { + const templateName = compMeta.inputs[propName]; + // Don't quote so that the key gets minified... + inputsExprs.push(new o.LiteralMapEntry(propName, o.literal(templateName), false)); + } + const outputsExprs: o.LiteralMapEntry[] = []; + for (let propName in compMeta.outputs) { + const templateName = compMeta.outputs[propName]; + // Don't quote so that the key gets minified... + outputsExprs.push(new o.LiteralMapEntry(propName, o.literal(templateName), false)); + } + + outputCtx.statements.push( + o.variable(compFactoryVar) + .set(o.importExpr(Identifiers.createComponentFactory).callFn([ + o.literal(compMeta.selector), outputCtx.importExpr(compMeta.type.reference), + o.variable(hostViewFactoryVar), new o.LiteralMapExpr(inputsExprs), + new o.LiteralMapExpr(outputsExprs), + o.literalArr( + compMeta.template !.ngContentSelectors.map(selector => o.literal(selector))) + ])) + .toDeclStmt( + o.importType( + Identifiers.ComponentFactory, + [o.expressionType(outputCtx.importExpr(compMeta.type.reference)) !], + [o.TypeModifier.Const]), + [o.StmtModifier.Final, o.StmtModifier.Exported])); + } + + private _compileComponent( + outputCtx: OutputContext, compMeta: CompileDirectiveMetadata, + ngModule: CompileNgModuleMetadata, directiveIdentifiers: CompileIdentifierMetadata[], + componentStyles: CompiledStylesheet|null, fileSuffix: string): ViewCompileResult { + const directives = + directiveIdentifiers.map(dir => this._metadataResolver.getDirectiveSummary(dir.reference)); + const pipes = ngModule.transitiveModule.pipes.map( + pipe => this._metadataResolver.getPipeSummary(pipe.reference)); + + const preserveWhitespaces = compMeta !.template !.preserveWhitespaces; + const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse( + compMeta, compMeta.template !.template !, directives, pipes, ngModule.schemas, + templateSourceUrl(ngModule.type, compMeta, compMeta.template !), preserveWhitespaces); + const stylesExpr = componentStyles ? o.variable(componentStyles.stylesVar) : o.literalArr([]); + const viewResult = this._viewCompiler.compileComponent( + outputCtx, compMeta, parsedTemplate, stylesExpr, usedPipes); + if (componentStyles) { + _resolveStyleStatements( + this._symbolResolver, componentStyles, this._styleCompiler.needsStyleShim(compMeta), + fileSuffix); + } + return viewResult; + } + + private _createOutputContext(genFilePath: string): OutputContext { + const importExpr = (symbol: StaticSymbol, typeParams: o.Type[] | null = null) => { + if (!(symbol instanceof StaticSymbol)) { + throw new Error(`Internal error: unknown identifier ${JSON.stringify(symbol)}`); + } + const arity = this._symbolResolver.getTypeArity(symbol) || 0; + const {filePath, name, members} = this._symbolResolver.getImportAs(symbol) || symbol; + const importModule = this._symbolResolver.fileNameToModuleName(filePath, genFilePath); + + // It should be good enough to compare filePath to genFilePath and if they are equal + // there is a self reference. However, ngfactory files generate to .ts but their + // symbols have .d.ts so a simple compare is insufficient. They should be canonical + // and is tracked by #17705. + const selfReference = this._symbolResolver.fileNameToModuleName(genFilePath, genFilePath); + const moduleName = importModule === selfReference ? null : importModule; + + // If we are in a type expression that refers to a generic type then supply + // the required type parameters. If there were not enough type parameters + // supplied, supply any as the type. Outside a type expression the reference + // should not supply type parameters and be treated as a simple value reference + // to the constructor function itself. + const suppliedTypeParams = typeParams || []; + const missingTypeParamsCount = arity - suppliedTypeParams.length; + const allTypeParams = + suppliedTypeParams.concat(new Array(missingTypeParamsCount).fill(o.DYNAMIC_TYPE)); + return members.reduce( + (expr, memberName) => expr.prop(memberName), + o.importExpr( + new o.ExternalReference(moduleName, name, null), allTypeParams)); + }; + + return {statements: [], genFilePath, importExpr}; + } + + private _codegenStyles( + srcFileUrl: string, compMeta: CompileDirectiveMetadata, + stylesheetMetadata: CompileStylesheetMetadata, fileSuffix: string): GeneratedFile { + const outputCtx = this._createOutputContext(_stylesModuleUrl( + stylesheetMetadata.moduleUrl !, this._styleCompiler.needsStyleShim(compMeta), fileSuffix)); + const compiledStylesheet = + this._styleCompiler.compileStyles(outputCtx, compMeta, stylesheetMetadata); + _resolveStyleStatements( + this._symbolResolver, compiledStylesheet, this._styleCompiler.needsStyleShim(compMeta), + fileSuffix); + return this._codegenSourceModule(srcFileUrl, outputCtx); + } + + private _codegenSourceModule(srcFileUrl: string, ctx: OutputContext): GeneratedFile { + return new GeneratedFile(srcFileUrl, ctx.genFilePath, ctx.statements); + } +} + +function _createTypeReferenceStub(outputCtx: OutputContext, reference: o.ExternalReference) { + outputCtx.statements.push(o.importExpr(reference).toStmt()); +} + +function _resolveStyleStatements( + symbolResolver: StaticSymbolResolver, compileResult: CompiledStylesheet, needsShim: boolean, + fileSuffix: string): void { + compileResult.dependencies.forEach((dep) => { + dep.setValue(symbolResolver.getStaticSymbol( + _stylesModuleUrl(dep.moduleUrl, needsShim, fileSuffix), dep.name)); + }); +} + +function _stylesModuleUrl(stylesheetUrl: string, shim: boolean, suffix: string): string { + return `${stylesheetUrl}${shim ? '.shim' : ''}.ngstyle${suffix}`; +} + +export interface NgAnalyzedModules { + ngModules: CompileNgModuleMetadata[]; + ngModuleByPipeOrDirective: Map; + files: Array<{ + srcUrl: string, + directives: StaticSymbol[], + pipes: StaticSymbol[], + ngModules: StaticSymbol[], + injectables: StaticSymbol[] + }>; + symbolsMissingModule?: StaticSymbol[]; +} + +export interface NgAnalyzeModulesHost { isSourceFile(filePath: string): boolean; } + +// Returns all the source files and a mapping from modules to directives +export function analyzeNgModules( + programStaticSymbols: StaticSymbol[], host: NgAnalyzeModulesHost, + metadataResolver: CompileMetadataResolver): NgAnalyzedModules { + const {ngModules, symbolsMissingModule} = + _createNgModules(programStaticSymbols, host, metadataResolver); + return _analyzeNgModules(programStaticSymbols, ngModules, symbolsMissingModule, metadataResolver); +} + +export function analyzeAndValidateNgModules( + programStaticSymbols: StaticSymbol[], host: NgAnalyzeModulesHost, + metadataResolver: CompileMetadataResolver): NgAnalyzedModules { + const result = analyzeNgModules(programStaticSymbols, host, metadataResolver); + if (result.symbolsMissingModule && result.symbolsMissingModule.length) { + const messages = result.symbolsMissingModule.map( + s => + `Cannot determine the module for class ${s.name} in ${s.filePath}! Add ${s.name} to the NgModule to fix it.`); + throw syntaxError(messages.join('\n')); + } + return result; +} + +function _analyzeNgModules( + programSymbols: StaticSymbol[], ngModuleMetas: CompileNgModuleMetadata[], + symbolsMissingModule: StaticSymbol[], + metadataResolver: CompileMetadataResolver): NgAnalyzedModules { + const moduleMetasByRef = new Map(); + ngModuleMetas.forEach((ngModule) => moduleMetasByRef.set(ngModule.type.reference, ngModule)); + const ngModuleByPipeOrDirective = new Map(); + const ngModulesByFile = new Map(); + const ngDirectivesByFile = new Map(); + const ngPipesByFile = new Map(); + const ngInjectablesByFile = new Map(); + const filePaths = new Set(); + + // Make sure we produce an analyzed file for each input file + programSymbols.forEach((symbol) => { + const filePath = symbol.filePath; + filePaths.add(filePath); + if (metadataResolver.isInjectable(symbol)) { + ngInjectablesByFile.set(filePath, (ngInjectablesByFile.get(filePath) || []).concat(symbol)); + } + }); + + // Looping over all modules to construct: + // - a map from file to modules `ngModulesByFile`, + // - a map from file to directives `ngDirectivesByFile`, + // - a map from file to pipes `ngPipesByFile`, + // - a map from directive/pipe to module `ngModuleByPipeOrDirective`. + ngModuleMetas.forEach((ngModuleMeta) => { + const srcFileUrl = ngModuleMeta.type.reference.filePath; + filePaths.add(srcFileUrl); + ngModulesByFile.set( + srcFileUrl, (ngModulesByFile.get(srcFileUrl) || []).concat(ngModuleMeta.type.reference)); + + ngModuleMeta.declaredDirectives.forEach((dirIdentifier) => { + const fileUrl = dirIdentifier.reference.filePath; + filePaths.add(fileUrl); + ngDirectivesByFile.set( + fileUrl, (ngDirectivesByFile.get(fileUrl) || []).concat(dirIdentifier.reference)); + ngModuleByPipeOrDirective.set(dirIdentifier.reference, ngModuleMeta); + }); + ngModuleMeta.declaredPipes.forEach((pipeIdentifier) => { + const fileUrl = pipeIdentifier.reference.filePath; + filePaths.add(fileUrl); + ngPipesByFile.set( + fileUrl, (ngPipesByFile.get(fileUrl) || []).concat(pipeIdentifier.reference)); + ngModuleByPipeOrDirective.set(pipeIdentifier.reference, ngModuleMeta); + }); + }); + + const files: { + srcUrl: string, + directives: StaticSymbol[], + pipes: StaticSymbol[], + ngModules: StaticSymbol[], + injectables: StaticSymbol[] + }[] = []; + + filePaths.forEach((srcUrl) => { + const directives = ngDirectivesByFile.get(srcUrl) || []; + const pipes = ngPipesByFile.get(srcUrl) || []; + const ngModules = ngModulesByFile.get(srcUrl) || []; + const injectables = ngInjectablesByFile.get(srcUrl) || []; + files.push({srcUrl, directives, pipes, ngModules, injectables}); + }); + + return { + // map directive/pipe to module + ngModuleByPipeOrDirective, + // list modules and directives for every source file + files, + ngModules: ngModuleMetas, symbolsMissingModule + }; +} + +export function extractProgramSymbols( + staticSymbolResolver: StaticSymbolResolver, files: string[], + host: NgAnalyzeModulesHost): StaticSymbol[] { + const staticSymbols: StaticSymbol[] = []; + files.filter(fileName => host.isSourceFile(fileName)).forEach(sourceFile => { + staticSymbolResolver.getSymbolsOf(sourceFile).forEach((symbol) => { + const resolvedSymbol = staticSymbolResolver.resolveSymbol(symbol); + const symbolMeta = resolvedSymbol.metadata; + if (symbolMeta) { + if (symbolMeta.__symbolic != 'error') { + // Ignore symbols that are only included to record error information. + staticSymbols.push(resolvedSymbol.symbol); + } + } + }); + }); + + return staticSymbols; +} + +// Load the NgModules and check +// that all directives / pipes that are present in the program +// are also declared by a module. +function _createNgModules( + programStaticSymbols: StaticSymbol[], host: NgAnalyzeModulesHost, + metadataResolver: CompileMetadataResolver): + {ngModules: CompileNgModuleMetadata[], symbolsMissingModule: StaticSymbol[]} { + const ngModules = new Map(); + const programPipesAndDirectives: StaticSymbol[] = []; + const ngModulePipesAndDirective = new Set(); + + const addNgModule = (staticSymbol: any) => { + if (ngModules.has(staticSymbol) || !host.isSourceFile(staticSymbol.filePath)) { + return false; + } + const ngModule = metadataResolver.getNgModuleMetadata(staticSymbol, false); + if (ngModule) { + ngModules.set(ngModule.type.reference, ngModule); + ngModule.declaredDirectives.forEach((dir) => ngModulePipesAndDirective.add(dir.reference)); + ngModule.declaredPipes.forEach((pipe) => ngModulePipesAndDirective.add(pipe.reference)); + // For every input module add the list of transitively included modules + ngModule.transitiveModule.modules.forEach(modMeta => addNgModule(modMeta.reference)); + } + return !!ngModule; + }; + programStaticSymbols.forEach((staticSymbol) => { + if (!addNgModule(staticSymbol) && + (metadataResolver.isDirective(staticSymbol) || metadataResolver.isPipe(staticSymbol))) { + programPipesAndDirectives.push(staticSymbol); + } + }); + + // Throw an error if any of the program pipe or directives is not declared by a module + const symbolsMissingModule = + programPipesAndDirectives.filter(s => !ngModulePipesAndDirective.has(s)); + + return {ngModules: Array.from(ngModules.values()), symbolsMissingModule}; +} diff --git a/angular/compiler/src/aot/compiler_factory.ts b/angular/compiler/src/aot/compiler_factory.ts new file mode 100644 index 0000000..f8155da --- /dev/null +++ b/angular/compiler/src/aot/compiler_factory.ts @@ -0,0 +1,90 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {CompilerConfig} from '../config'; +import {MissingTranslationStrategy, ViewEncapsulation} from '../core'; +import {DirectiveNormalizer} from '../directive_normalizer'; +import {DirectiveResolver} from '../directive_resolver'; +import {Lexer} from '../expression_parser/lexer'; +import {Parser} from '../expression_parser/parser'; +import {I18NHtmlParser} from '../i18n/i18n_html_parser'; +import {CompileMetadataResolver} from '../metadata_resolver'; +import {HtmlParser} from '../ml_parser/html_parser'; +import {NgModuleCompiler} from '../ng_module_compiler'; +import {NgModuleResolver} from '../ng_module_resolver'; +import {TypeScriptEmitter} from '../output/ts_emitter'; +import {PipeResolver} from '../pipe_resolver'; +import {DomElementSchemaRegistry} from '../schema/dom_element_schema_registry'; +import {StyleCompiler} from '../style_compiler'; +import {TemplateParser} from '../template_parser/template_parser'; +import {UrlResolver} from '../url_resolver'; +import {syntaxError} from '../util'; +import {ViewCompiler} from '../view_compiler/view_compiler'; + +import {AotCompiler} from './compiler'; +import {AotCompilerHost} from './compiler_host'; +import {AotCompilerOptions} from './compiler_options'; +import {StaticReflector} from './static_reflector'; +import {StaticSymbol, StaticSymbolCache} from './static_symbol'; +import {StaticSymbolResolver} from './static_symbol_resolver'; +import {AotSummaryResolver} from './summary_resolver'; + +export function createAotUrlResolver(host: { + resourceNameToFileName(resourceName: string, containingFileName: string): string | null; +}): UrlResolver { + return { + resolve: (basePath: string, url: string) => { + const filePath = host.resourceNameToFileName(url, basePath); + if (!filePath) { + throw syntaxError(`Couldn't resolve resource ${url} from ${basePath}`); + } + return filePath; + } + }; +} + +/** + * Creates a new AotCompiler based on options and a host. + */ +export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCompilerOptions): + {compiler: AotCompiler, reflector: StaticReflector} { + let translations: string = options.translations || ''; + + const urlResolver = createAotUrlResolver(compilerHost); + const symbolCache = new StaticSymbolCache(); + const summaryResolver = new AotSummaryResolver(compilerHost, symbolCache); + const symbolResolver = new StaticSymbolResolver(compilerHost, symbolCache, summaryResolver); + const staticReflector = new StaticReflector(summaryResolver, symbolResolver); + const htmlParser = new I18NHtmlParser( + new HtmlParser(), translations, options.i18nFormat, options.missingTranslation, console); + const config = new CompilerConfig({ + defaultEncapsulation: ViewEncapsulation.Emulated, + useJit: false, + enableLegacyTemplate: options.enableLegacyTemplate !== false, + missingTranslation: options.missingTranslation, + preserveWhitespaces: options.preserveWhitespaces, + }); + const normalizer = new DirectiveNormalizer( + {get: (url: string) => compilerHost.loadResource(url)}, urlResolver, htmlParser, config); + const expressionParser = new Parser(new Lexer()); + const elementSchemaRegistry = new DomElementSchemaRegistry(); + const tmplParser = new TemplateParser( + config, staticReflector, expressionParser, elementSchemaRegistry, htmlParser, console, []); + const resolver = new CompileMetadataResolver( + config, new NgModuleResolver(staticReflector), new DirectiveResolver(staticReflector), + new PipeResolver(staticReflector), summaryResolver, elementSchemaRegistry, normalizer, + console, symbolCache, staticReflector); + // TODO(vicb): do not pass options.i18nFormat here + const viewCompiler = new ViewCompiler(config, staticReflector, elementSchemaRegistry); + const compiler = new AotCompiler( + config, compilerHost, staticReflector, resolver, tmplParser, new StyleCompiler(urlResolver), + viewCompiler, new NgModuleCompiler(staticReflector), new TypeScriptEmitter(), summaryResolver, + options.locale || null, options.i18nFormat || null, options.enableSummariesForJit || null, + symbolResolver); + return {compiler, reflector: staticReflector}; +} diff --git a/angular/compiler/src/aot/compiler_host.ts b/angular/compiler/src/aot/compiler_host.ts new file mode 100644 index 0000000..00bbf1e --- /dev/null +++ b/angular/compiler/src/aot/compiler_host.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {StaticSymbolResolverHost} from './static_symbol_resolver'; +import {AotSummaryResolverHost} from './summary_resolver'; + +/** + * The host of the AotCompiler disconnects the implementation from TypeScript / other language + * services and from underlying file systems. + */ +export interface AotCompilerHost extends StaticSymbolResolverHost, AotSummaryResolverHost { + /** + * Converts a path that refers to a resource into an absolute filePath + * that can be later on used for loading the resource via `loadResource. + */ + resourceNameToFileName(resourceName: string, containingFileName: string): string|null; + /** + * Loads a resource (e.g. html / css) + */ + loadResource(path: string): Promise|string; +} diff --git a/angular/compiler/src/aot/compiler_options.ts b/angular/compiler/src/aot/compiler_options.ts new file mode 100644 index 0000000..5528e9b --- /dev/null +++ b/angular/compiler/src/aot/compiler_options.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {MissingTranslationStrategy} from '../core'; + +export interface AotCompilerOptions { + locale?: string; + i18nFormat?: string; + translations?: string; + missingTranslation?: MissingTranslationStrategy; + enableLegacyTemplate?: boolean; + enableSummariesForJit?: boolean; + preserveWhitespaces?: boolean; +} diff --git a/angular/compiler/src/aot/generated_file.ts b/angular/compiler/src/aot/generated_file.ts new file mode 100644 index 0000000..226126f --- /dev/null +++ b/angular/compiler/src/aot/generated_file.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {sourceUrl} from '../compile_metadata'; +import {Statement} from '../output/output_ast'; +import {TypeScriptEmitter} from '../output/ts_emitter'; + +export class GeneratedFile { + public source: string|null; + public stmts: Statement[]|null; + + constructor( + public srcFileUrl: string, public genFileUrl: string, sourceOrStmts: string|Statement[]) { + if (typeof sourceOrStmts === 'string') { + this.source = sourceOrStmts; + this.stmts = null; + } else { + this.source = null; + this.stmts = sourceOrStmts; + } + } +} + +export function toTypeScript(file: GeneratedFile, preamble: string = ''): string { + if (!file.stmts) { + throw new Error(`Illegal state: No stmts present on GeneratedFile ${file.genFileUrl}`); + } + return new TypeScriptEmitter().emitStatements( + sourceUrl(file.srcFileUrl), file.genFileUrl, file.stmts, preamble); +} diff --git a/angular/compiler/src/aot/static_reflector.ts b/angular/compiler/src/aot/static_reflector.ts new file mode 100644 index 0000000..0a72ad9 --- /dev/null +++ b/angular/compiler/src/aot/static_reflector.ts @@ -0,0 +1,751 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {CompileSummaryKind} from '../compile_metadata'; +import {CompileReflector} from '../compile_reflector'; +import {MetadataFactory, createAttribute, createComponent, createContentChild, createContentChildren, createDirective, createHost, createHostBinding, createHostListener, createInject, createInjectable, createInput, createNgModule, createOptional, createOutput, createPipe, createSelf, createSkipSelf, createViewChild, createViewChildren} from '../core'; +import * as o from '../output/output_ast'; +import {SummaryResolver} from '../summary_resolver'; +import {syntaxError} from '../util'; + +import {StaticSymbol} from './static_symbol'; +import {StaticSymbolResolver} from './static_symbol_resolver'; + +const ANGULAR_CORE = '@angular/core'; +const ANGULAR_ROUTER = '@angular/router'; + +const HIDDEN_KEY = /^\$.*\$$/; + +const IGNORE = { + __symbolic: 'ignore' +}; + +const USE_VALUE = 'useValue'; +const PROVIDE = 'provide'; +const REFERENCE_SET = new Set([USE_VALUE, 'useFactory', 'data']); + +function shouldIgnore(value: any): boolean { + return value && value.__symbolic == 'ignore'; +} + +/** + * A static reflector implements enough of the Reflector API that is necessary to compile + * templates statically. + */ +export class StaticReflector implements CompileReflector { + private annotationCache = new Map(); + private propertyCache = new Map(); + private parameterCache = new Map(); + private methodCache = new Map(); + private conversionMap = new Map any>(); + private injectionToken: StaticSymbol; + private opaqueToken: StaticSymbol; + private ROUTES: StaticSymbol; + private ANALYZE_FOR_ENTRY_COMPONENTS: StaticSymbol; + private annotationForParentClassWithSummaryKind = + new Map[]>(); + + constructor( + private summaryResolver: SummaryResolver, + private symbolResolver: StaticSymbolResolver, + knownMetadataClasses: {name: string, filePath: string, ctor: any}[] = [], + knownMetadataFunctions: {name: string, filePath: string, fn: any}[] = [], + private errorRecorder?: (error: any, fileName?: string) => void) { + this.initializeConversionMap(); + knownMetadataClasses.forEach( + (kc) => this._registerDecoratorOrConstructor( + this.getStaticSymbol(kc.filePath, kc.name), kc.ctor)); + knownMetadataFunctions.forEach( + (kf) => this._registerFunction(this.getStaticSymbol(kf.filePath, kf.name), kf.fn)); + this.annotationForParentClassWithSummaryKind.set( + CompileSummaryKind.Directive, [createDirective, createComponent]); + this.annotationForParentClassWithSummaryKind.set(CompileSummaryKind.Pipe, [createPipe]); + this.annotationForParentClassWithSummaryKind.set(CompileSummaryKind.NgModule, [createNgModule]); + this.annotationForParentClassWithSummaryKind.set( + CompileSummaryKind.Injectable, + [createInjectable, createPipe, createDirective, createComponent, createNgModule]); + } + + componentModuleUrl(typeOrFunc: StaticSymbol): string { + const staticSymbol = this.findSymbolDeclaration(typeOrFunc); + return this.symbolResolver.getResourcePath(staticSymbol); + } + + resolveExternalReference(ref: o.ExternalReference): StaticSymbol { + const refSymbol = this.symbolResolver.getSymbolByModule(ref.moduleName !, ref.name !); + const declarationSymbol = this.findSymbolDeclaration(refSymbol); + this.symbolResolver.recordModuleNameForFileName(refSymbol.filePath, ref.moduleName !); + this.symbolResolver.recordImportAs(declarationSymbol, refSymbol); + return declarationSymbol; + } + + findDeclaration(moduleUrl: string, name: string, containingFile?: string): StaticSymbol { + return this.findSymbolDeclaration( + this.symbolResolver.getSymbolByModule(moduleUrl, name, containingFile)); + } + + tryFindDeclaration(moduleUrl: string, name: string): StaticSymbol { + return this.symbolResolver.ignoreErrorsFor(() => this.findDeclaration(moduleUrl, name)); + } + + findSymbolDeclaration(symbol: StaticSymbol): StaticSymbol { + const resolvedSymbol = this.symbolResolver.resolveSymbol(symbol); + if (resolvedSymbol && resolvedSymbol.metadata instanceof StaticSymbol) { + return this.findSymbolDeclaration(resolvedSymbol.metadata); + } else { + return symbol; + } + } + + public annotations(type: StaticSymbol): any[] { + let annotations = this.annotationCache.get(type); + if (!annotations) { + annotations = []; + const classMetadata = this.getTypeMetadata(type); + const parentType = this.findParentType(type, classMetadata); + if (parentType) { + const parentAnnotations = this.annotations(parentType); + annotations.push(...parentAnnotations); + } + let ownAnnotations: any[] = []; + if (classMetadata['decorators']) { + ownAnnotations = this.simplify(type, classMetadata['decorators']); + annotations.push(...ownAnnotations); + } + if (parentType && !this.summaryResolver.isLibraryFile(type.filePath) && + this.summaryResolver.isLibraryFile(parentType.filePath)) { + const summary = this.summaryResolver.resolveSummary(parentType); + if (summary && summary.type) { + const requiredAnnotationTypes = + this.annotationForParentClassWithSummaryKind.get(summary.type.summaryKind !) !; + const typeHasRequiredAnnotation = requiredAnnotationTypes.some( + (requiredType) => ownAnnotations.some(ann => requiredType.isTypeOf(ann))); + if (!typeHasRequiredAnnotation) { + this.reportError( + syntaxError( + `Class ${type.name} in ${type.filePath} extends from a ${CompileSummaryKind[summary.type.summaryKind!]} in another compilation unit without duplicating the decorator. ` + + `Please add a ${requiredAnnotationTypes.map((type) => type.ngMetadataName).join(' or ')} decorator to the class.`), + type); + } + } + } + this.annotationCache.set(type, annotations.filter(ann => !!ann)); + } + return annotations; + } + + public propMetadata(type: StaticSymbol): {[key: string]: any[]} { + let propMetadata = this.propertyCache.get(type); + if (!propMetadata) { + const classMetadata = this.getTypeMetadata(type); + propMetadata = {}; + const parentType = this.findParentType(type, classMetadata); + if (parentType) { + const parentPropMetadata = this.propMetadata(parentType); + Object.keys(parentPropMetadata).forEach((parentProp) => { + propMetadata ![parentProp] = parentPropMetadata[parentProp]; + }); + } + + const members = classMetadata['members'] || {}; + Object.keys(members).forEach((propName) => { + const propData = members[propName]; + const prop = (propData) + .find(a => a['__symbolic'] == 'property' || a['__symbolic'] == 'method'); + const decorators: any[] = []; + if (propMetadata ![propName]) { + decorators.push(...propMetadata ![propName]); + } + propMetadata ![propName] = decorators; + if (prop && prop['decorators']) { + decorators.push(...this.simplify(type, prop['decorators'])); + } + }); + this.propertyCache.set(type, propMetadata); + } + return propMetadata; + } + + public parameters(type: StaticSymbol): any[] { + if (!(type instanceof StaticSymbol)) { + this.reportError( + new Error(`parameters received ${JSON.stringify(type)} which is not a StaticSymbol`), + type); + return []; + } + try { + let parameters = this.parameterCache.get(type); + if (!parameters) { + const classMetadata = this.getTypeMetadata(type); + const parentType = this.findParentType(type, classMetadata); + const members = classMetadata ? classMetadata['members'] : null; + const ctorData = members ? members['__ctor__'] : null; + if (ctorData) { + const ctor = (ctorData).find(a => a['__symbolic'] == 'constructor'); + const rawParameterTypes = ctor['parameters'] || []; + const parameterDecorators = this.simplify(type, ctor['parameterDecorators'] || []); + parameters = []; + rawParameterTypes.forEach((rawParamType, index) => { + const nestedResult: any[] = []; + const paramType = this.trySimplify(type, rawParamType); + if (paramType) nestedResult.push(paramType); + const decorators = parameterDecorators ? parameterDecorators[index] : null; + if (decorators) { + nestedResult.push(...decorators); + } + parameters !.push(nestedResult); + }); + } else if (parentType) { + parameters = this.parameters(parentType); + } + if (!parameters) { + parameters = []; + } + this.parameterCache.set(type, parameters); + } + return parameters; + } catch (e) { + console.error(`Failed on type ${JSON.stringify(type)} with error ${e}`); + throw e; + } + } + + private _methodNames(type: any): {[key: string]: boolean} { + let methodNames = this.methodCache.get(type); + if (!methodNames) { + const classMetadata = this.getTypeMetadata(type); + methodNames = {}; + const parentType = this.findParentType(type, classMetadata); + if (parentType) { + const parentMethodNames = this._methodNames(parentType); + Object.keys(parentMethodNames).forEach((parentProp) => { + methodNames ![parentProp] = parentMethodNames[parentProp]; + }); + } + + const members = classMetadata['members'] || {}; + Object.keys(members).forEach((propName) => { + const propData = members[propName]; + const isMethod = (propData).some(a => a['__symbolic'] == 'method'); + methodNames ![propName] = methodNames ![propName] || isMethod; + }); + this.methodCache.set(type, methodNames); + } + return methodNames; + } + + private findParentType(type: StaticSymbol, classMetadata: any): StaticSymbol|undefined { + const parentType = this.trySimplify(type, classMetadata['extends']); + if (parentType instanceof StaticSymbol) { + return parentType; + } + } + + hasLifecycleHook(type: any, lcProperty: string): boolean { + if (!(type instanceof StaticSymbol)) { + this.reportError( + new Error( + `hasLifecycleHook received ${JSON.stringify(type)} which is not a StaticSymbol`), + type); + } + try { + return !!this._methodNames(type)[lcProperty]; + } catch (e) { + console.error(`Failed on type ${JSON.stringify(type)} with error ${e}`); + throw e; + } + } + + private _registerDecoratorOrConstructor(type: StaticSymbol, ctor: any): void { + this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => new ctor(...args)); + } + + private _registerFunction(type: StaticSymbol, fn: any): void { + this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => fn.apply(undefined, args)); + } + + private initializeConversionMap(): void { + this.injectionToken = this.findDeclaration(ANGULAR_CORE, 'InjectionToken'); + this.opaqueToken = this.findDeclaration(ANGULAR_CORE, 'OpaqueToken'); + this.ROUTES = this.tryFindDeclaration(ANGULAR_ROUTER, 'ROUTES'); + this.ANALYZE_FOR_ENTRY_COMPONENTS = + this.findDeclaration(ANGULAR_CORE, 'ANALYZE_FOR_ENTRY_COMPONENTS'); + + this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Host'), createHost); + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'Injectable'), createInjectable); + this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Self'), createSelf); + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'SkipSelf'), createSkipSelf); + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'Inject'), createInject); + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'Optional'), createOptional); + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'Attribute'), createAttribute); + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'ContentChild'), createContentChild); + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'ContentChildren'), createContentChildren); + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'ViewChild'), createViewChild); + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'ViewChildren'), createViewChildren); + this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Input'), createInput); + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'Output'), createOutput); + this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Pipe'), createPipe); + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'HostBinding'), createHostBinding); + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'HostListener'), createHostListener); + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'Directive'), createDirective); + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'Component'), createComponent); + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'NgModule'), createNgModule); + + // Note: Some metadata classes can be used directly with Provider.deps. + this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Host'), createHost); + this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Self'), createSelf); + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'SkipSelf'), createSkipSelf); + this._registerDecoratorOrConstructor( + this.findDeclaration(ANGULAR_CORE, 'Optional'), createOptional); + } + + /** + * getStaticSymbol produces a Type whose metadata is known but whose implementation is not loaded. + * All types passed to the StaticResolver should be pseudo-types returned by this method. + * + * @param declarationFile the absolute path of the file where the symbol is declared + * @param name the name of the type. + */ + getStaticSymbol(declarationFile: string, name: string, members?: string[]): StaticSymbol { + return this.symbolResolver.getStaticSymbol(declarationFile, name, members); + } + + private reportError(error: Error, context: StaticSymbol, path?: string) { + if (this.errorRecorder) { + this.errorRecorder(error, (context && context.filePath) || path); + } else { + throw error; + } + } + + /** + * Simplify but discard any errors + */ + private trySimplify(context: StaticSymbol, value: any): any { + const originalRecorder = this.errorRecorder; + this.errorRecorder = (error: any, fileName: string) => {}; + const result = this.simplify(context, value); + this.errorRecorder = originalRecorder; + return result; + } + + /** @internal */ + public simplify(context: StaticSymbol, value: any): any { + const self = this; + let scope = BindingScope.empty; + const calling = new Map(); + + function simplifyInContext( + context: StaticSymbol, value: any, depth: number, references: number): any { + function resolveReferenceValue(staticSymbol: StaticSymbol): any { + const resolvedSymbol = self.symbolResolver.resolveSymbol(staticSymbol); + return resolvedSymbol ? resolvedSymbol.metadata : null; + } + + function simplifyCall(functionSymbol: StaticSymbol, targetFunction: any, args: any[]) { + if (targetFunction && targetFunction['__symbolic'] == 'function') { + if (calling.get(functionSymbol)) { + throw new Error('Recursion not supported'); + } + try { + const value = targetFunction['value']; + if (value && (depth != 0 || value.__symbolic != 'error')) { + const parameters: string[] = targetFunction['parameters']; + const defaults: any[] = targetFunction.defaults; + args = args.map(arg => simplifyInContext(context, arg, depth + 1, references)) + .map(arg => shouldIgnore(arg) ? undefined : arg); + if (defaults && defaults.length > args.length) { + args.push(...defaults.slice(args.length).map((value: any) => simplify(value))); + } + calling.set(functionSymbol, true); + const functionScope = BindingScope.build(); + for (let i = 0; i < parameters.length; i++) { + functionScope.define(parameters[i], args[i]); + } + const oldScope = scope; + let result: any; + try { + scope = functionScope.done(); + result = simplifyInContext(functionSymbol, value, depth + 1, references); + } finally { + scope = oldScope; + } + return result; + } + } finally { + calling.delete(functionSymbol); + } + } + + if (depth === 0) { + // If depth is 0 we are evaluating the top level expression that is describing element + // decorator. In this case, it is a decorator we don't understand, such as a custom + // non-angular decorator, and we should just ignore it. + return IGNORE; + } + return simplify( + {__symbolic: 'error', message: 'Function call not supported', context: functionSymbol}); + } + + function simplify(expression: any): any { + if (isPrimitive(expression)) { + return expression; + } + if (expression instanceof Array) { + const result: any[] = []; + for (const item of (expression)) { + // Check for a spread expression + if (item && item.__symbolic === 'spread') { + const spreadArray = simplify(item.expression); + if (Array.isArray(spreadArray)) { + for (const spreadItem of spreadArray) { + result.push(spreadItem); + } + continue; + } + } + const value = simplify(item); + if (shouldIgnore(value)) { + continue; + } + result.push(value); + } + return result; + } + if (expression instanceof StaticSymbol) { + // Stop simplification at builtin symbols or if we are in a reference context + if (expression === self.injectionToken || expression === self.opaqueToken || + self.conversionMap.has(expression) || references > 0) { + return expression; + } else { + const staticSymbol = expression; + const declarationValue = resolveReferenceValue(staticSymbol); + if (declarationValue) { + return simplifyInContext(staticSymbol, declarationValue, depth + 1, references); + } else { + return staticSymbol; + } + } + } + if (expression) { + if (expression['__symbolic']) { + let staticSymbol: StaticSymbol; + switch (expression['__symbolic']) { + case 'binop': + let left = simplify(expression['left']); + if (shouldIgnore(left)) return left; + let right = simplify(expression['right']); + if (shouldIgnore(right)) return right; + switch (expression['operator']) { + case '&&': + return left && right; + case '||': + return left || right; + case '|': + return left | right; + case '^': + return left ^ right; + case '&': + return left & right; + case '==': + return left == right; + case '!=': + return left != right; + case '===': + return left === right; + case '!==': + return left !== right; + case '<': + return left < right; + case '>': + return left > right; + case '<=': + return left <= right; + case '>=': + return left >= right; + case '<<': + return left << right; + case '>>': + return left >> right; + case '+': + return left + right; + case '-': + return left - right; + case '*': + return left * right; + case '/': + return left / right; + case '%': + return left % right; + } + return null; + case 'if': + let condition = simplify(expression['condition']); + return condition ? simplify(expression['thenExpression']) : + simplify(expression['elseExpression']); + case 'pre': + let operand = simplify(expression['operand']); + if (shouldIgnore(operand)) return operand; + switch (expression['operator']) { + case '+': + return operand; + case '-': + return -operand; + case '!': + return !operand; + case '~': + return ~operand; + } + return null; + case 'index': + let indexTarget = simplify(expression['expression']); + let index = simplify(expression['index']); + if (indexTarget && isPrimitive(index)) return indexTarget[index]; + return null; + case 'select': + const member = expression['member']; + let selectContext = context; + let selectTarget = simplify(expression['expression']); + if (selectTarget instanceof StaticSymbol) { + const members = selectTarget.members.concat(member); + selectContext = + self.getStaticSymbol(selectTarget.filePath, selectTarget.name, members); + const declarationValue = resolveReferenceValue(selectContext); + if (declarationValue) { + return simplifyInContext( + selectContext, declarationValue, depth + 1, references); + } else { + return selectContext; + } + } + if (selectTarget && isPrimitive(member)) + return simplifyInContext( + selectContext, selectTarget[member], depth + 1, references); + return null; + case 'reference': + // Note: This only has to deal with variable references, + // as symbol references have been converted into StaticSymbols already + // in the StaticSymbolResolver! + const name: string = expression['name']; + const localValue = scope.resolve(name); + if (localValue != BindingScope.missing) { + return localValue; + } + break; + case 'class': + return context; + case 'function': + return context; + case 'new': + case 'call': + // Determine if the function is a built-in conversion + staticSymbol = simplifyInContext( + context, expression['expression'], depth + 1, /* references */ 0); + if (staticSymbol instanceof StaticSymbol) { + if (staticSymbol === self.injectionToken || staticSymbol === self.opaqueToken) { + // if somebody calls new InjectionToken, don't create an InjectionToken, + // but rather return the symbol to which the InjectionToken is assigned to. + return context; + } + const argExpressions: any[] = expression['arguments'] || []; + let converter = self.conversionMap.get(staticSymbol); + if (converter) { + const args = + argExpressions + .map(arg => simplifyInContext(context, arg, depth + 1, references)) + .map(arg => shouldIgnore(arg) ? undefined : arg); + return converter(context, args); + } else { + // Determine if the function is one we can simplify. + const targetFunction = resolveReferenceValue(staticSymbol); + return simplifyCall(staticSymbol, targetFunction, argExpressions); + } + } + return IGNORE; + case 'error': + let message = produceErrorMessage(expression); + if (expression['line']) { + message = + `${message} (position ${expression['line']+1}:${expression['character']+1} in the original .ts file)`; + self.reportError( + positionalError( + message, context.filePath, expression['line'], expression['character']), + context); + } else { + self.reportError(new Error(message), context); + } + return IGNORE; + case 'ignore': + return expression; + } + return null; + } + return mapStringMap(expression, (value, name) => { + if (REFERENCE_SET.has(name)) { + if (name === USE_VALUE && PROVIDE in expression) { + // If this is a provider expression, check for special tokens that need the value + // during analysis. + const provide = simplify(expression.provide); + if (provide === self.ROUTES || provide == self.ANALYZE_FOR_ENTRY_COMPONENTS) { + return simplify(value); + } + } + return simplifyInContext(context, value, depth, references + 1); + } + return simplify(value); + }); + } + return IGNORE; + } + + try { + return simplify(value); + } catch (e) { + const members = context.members.length ? `.${context.members.join('.')}` : ''; + const message = + `${e.message}, resolving symbol ${context.name}${members} in ${context.filePath}`; + if (e.fileName) { + throw positionalError(message, e.fileName, e.line, e.column); + } + throw syntaxError(message); + } + } + + const recordedSimplifyInContext = (context: StaticSymbol, value: any) => { + try { + return simplifyInContext(context, value, 0, 0); + } catch (e) { + this.reportError(e, context); + } + }; + + const result = this.errorRecorder ? recordedSimplifyInContext(context, value) : + simplifyInContext(context, value, 0, 0); + if (shouldIgnore(result)) { + return undefined; + } + return result; + } + + private getTypeMetadata(type: StaticSymbol): {[key: string]: any} { + const resolvedSymbol = this.symbolResolver.resolveSymbol(type); + return resolvedSymbol && resolvedSymbol.metadata ? resolvedSymbol.metadata : + {__symbolic: 'class'}; + } +} + +function expandedMessage(error: any): string { + switch (error.message) { + case 'Reference to non-exported class': + if (error.context && error.context.className) { + return `Reference to a non-exported class ${error.context.className}. Consider exporting the class`; + } + break; + case 'Variable not initialized': + return 'Only initialized variables and constants can be referenced because the value of this variable is needed by the template compiler'; + case 'Destructuring not supported': + return 'Referencing an exported destructured variable or constant is not supported by the template compiler. Consider simplifying this to avoid destructuring'; + case 'Could not resolve type': + if (error.context && error.context.typeName) { + return `Could not resolve type ${error.context.typeName}`; + } + break; + case 'Function call not supported': + let prefix = + error.context && error.context.name ? `Calling function '${error.context.name}', f` : 'F'; + return prefix + + 'unction calls are not supported. Consider replacing the function or lambda with a reference to an exported function'; + case 'Reference to a local symbol': + if (error.context && error.context.name) { + return `Reference to a local (non-exported) symbol '${error.context.name}'. Consider exporting the symbol`; + } + break; + } + return error.message; +} + +function produceErrorMessage(error: any): string { + return `Error encountered resolving symbol values statically. ${expandedMessage(error)}`; +} + +function mapStringMap(input: {[key: string]: any}, transform: (value: any, key: string) => any): + {[key: string]: any} { + if (!input) return {}; + const result: {[key: string]: any} = {}; + Object.keys(input).forEach((key) => { + const value = transform(input[key], key); + if (!shouldIgnore(value)) { + if (HIDDEN_KEY.test(key)) { + Object.defineProperty(result, key, {enumerable: false, configurable: true, value: value}); + } else { + result[key] = value; + } + } + }); + return result; +} + +function isPrimitive(o: any): boolean { + return o === null || (typeof o !== 'function' && typeof o !== 'object'); +} + +interface BindingScopeBuilder { + define(name: string, value: any): BindingScopeBuilder; + done(): BindingScope; +} + +abstract class BindingScope { + abstract resolve(name: string): any; + public static missing = {}; + public static empty: BindingScope = {resolve: name => BindingScope.missing}; + + public static build(): BindingScopeBuilder { + const current = new Map(); + return { + define: function(name, value) { + current.set(name, value); + return this; + }, + done: function() { + return current.size > 0 ? new PopulatedScope(current) : BindingScope.empty; + } + }; + } +} + +class PopulatedScope extends BindingScope { + constructor(private bindings: Map) { super(); } + + resolve(name: string): any { + return this.bindings.has(name) ? this.bindings.get(name) : BindingScope.missing; + } +} + +function positionalError(message: string, fileName: string, line: number, column: number): Error { + const result = new Error(message); + (result as any).fileName = fileName; + (result as any).line = line; + (result as any).column = column; + return result; +} \ No newline at end of file diff --git a/angular/compiler/src/aot/static_symbol.ts b/angular/compiler/src/aot/static_symbol.ts new file mode 100644 index 0000000..cb47dd9 --- /dev/null +++ b/angular/compiler/src/aot/static_symbol.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * A token representing the a reference to a static type. + * + * This token is unique for a filePath and name and can be used as a hash table key. + */ +export class StaticSymbol { + constructor(public filePath: string, public name: string, public members: string[]) {} + + assertNoMembers() { + if (this.members.length) { + throw new Error( + `Illegal state: symbol without members expected, but got ${JSON.stringify(this)}.`); + } + } +} + +/** + * A cache of static symbol used by the StaticReflector to return the same symbol for the + * same symbol values. + */ +export class StaticSymbolCache { + private cache = new Map(); + + get(declarationFile: string, name: string, members?: string[]): StaticSymbol { + members = members || []; + const memberSuffix = members.length ? `.${ members.join('.')}` : ''; + const key = `"${declarationFile}".${name}${memberSuffix}`; + let result = this.cache.get(key); + if (!result) { + result = new StaticSymbol(declarationFile, name, members); + this.cache.set(key, result); + } + return result; + } +} \ No newline at end of file diff --git a/angular/compiler/src/aot/static_symbol_resolver.ts b/angular/compiler/src/aot/static_symbol_resolver.ts new file mode 100644 index 0000000..6a66d3a --- /dev/null +++ b/angular/compiler/src/aot/static_symbol_resolver.ts @@ -0,0 +1,492 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {SummaryResolver} from '../summary_resolver'; +import {ValueTransformer, visitValue} from '../util'; + +import {StaticSymbol, StaticSymbolCache} from './static_symbol'; +import {isGeneratedFile, stripSummaryForJitFileSuffix, stripSummaryForJitNameSuffix, summaryForJitFileName, summaryForJitName} from './util'; + +export class ResolvedStaticSymbol { + constructor(public symbol: StaticSymbol, public metadata: any) {} +} + +/** + * The host of the SymbolResolverHost disconnects the implementation from TypeScript / other + * language + * services and from underlying file systems. + */ +export interface StaticSymbolResolverHost { + /** + * Return a ModuleMetadata for the given module. + * Angular CLI will produce this metadata for a module whenever a .d.ts files is + * produced and the module has exported variables or classes with decorators. Module metadata can + * also be produced directly from TypeScript sources by using MetadataCollector in tools/metadata. + * + * @param modulePath is a string identifier for a module as an absolute path. + * @returns the metadata for the given module. + */ + getMetadataFor(modulePath: string): {[key: string]: any}[]|undefined; + + /** + * Converts a module name that is used in an `import` to a file path. + * I.e. + * `path/to/containingFile.ts` containing `import {...} from 'module-name'`. + */ + moduleNameToFileName(moduleName: string, containingFile?: string): string|null; + /** + * Converts a file path to a module name that can be used as an `import. + * I.e. `path/to/importedFile.ts` should be imported by `path/to/containingFile.ts`. + * + * See ImportResolver. + */ + fileNameToModuleName(importedFilePath: string, containingFilePath: string): string|null; +} + +const SUPPORTED_SCHEMA_VERSION = 3; + +/** + * This class is responsible for loading metadata per symbol, + * and normalizing references between symbols. + * + * Internally, it only uses symbols without members, + * and deduces the values for symbols with members based + * on these symbols. + */ +export class StaticSymbolResolver { + private metadataCache = new Map(); + // Note: this will only contain StaticSymbols without members! + private resolvedSymbols = new Map(); + private resolvedFilePaths = new Set(); + // Note: this will only contain StaticSymbols without members! + private importAs = new Map(); + private symbolResourcePaths = new Map(); + private symbolFromFile = new Map(); + private knownFileNameToModuleNames = new Map(); + + constructor( + private host: StaticSymbolResolverHost, private staticSymbolCache: StaticSymbolCache, + private summaryResolver: SummaryResolver, + private errorRecorder?: (error: any, fileName?: string) => void) {} + + resolveSymbol(staticSymbol: StaticSymbol): ResolvedStaticSymbol { + if (staticSymbol.members.length > 0) { + return this._resolveSymbolMembers(staticSymbol) !; + } + let result = this.resolvedSymbols.get(staticSymbol); + if (result) { + return result; + } + result = this._resolveSymbolFromSummary(staticSymbol) !; + if (result) { + return result; + } + // Note: Some users use libraries that were not compiled with ngc, i.e. they don't + // have summaries, only .d.ts files. So we always need to check both, the summary + // and metadata. + this._createSymbolsOf(staticSymbol.filePath); + result = this.resolvedSymbols.get(staticSymbol) !; + return result; + } + + /** + * getImportAs produces a symbol that can be used to import the given symbol. + * The import might be different than the symbol if the symbol is exported from + * a library with a summary; in which case we want to import the symbol from the + * ngfactory re-export instead of directly to avoid introducing a direct dependency + * on an otherwise indirect dependency. + * + * @param staticSymbol the symbol for which to generate a import symbol + */ + getImportAs(staticSymbol: StaticSymbol): StaticSymbol|null { + if (staticSymbol.members.length) { + const baseSymbol = this.getStaticSymbol(staticSymbol.filePath, staticSymbol.name); + const baseImportAs = this.getImportAs(baseSymbol); + return baseImportAs ? + this.getStaticSymbol(baseImportAs.filePath, baseImportAs.name, staticSymbol.members) : + null; + } + const summarizedFileName = stripSummaryForJitFileSuffix(staticSymbol.filePath); + if (summarizedFileName !== staticSymbol.filePath) { + const summarizedName = stripSummaryForJitNameSuffix(staticSymbol.name); + const baseSymbol = + this.getStaticSymbol(summarizedFileName, summarizedName, staticSymbol.members); + const baseImportAs = this.getImportAs(baseSymbol); + return baseImportAs ? + this.getStaticSymbol( + summaryForJitFileName(baseImportAs.filePath), summaryForJitName(baseImportAs.name), + baseSymbol.members) : + null; + } + let result = this.summaryResolver.getImportAs(staticSymbol); + if (!result) { + result = this.importAs.get(staticSymbol) !; + } + return result; + } + + /** + * getResourcePath produces the path to the original location of the symbol and should + * be used to determine the relative location of resource references recorded in + * symbol metadata. + */ + getResourcePath(staticSymbol: StaticSymbol): string { + return this.symbolResourcePaths.get(staticSymbol) || staticSymbol.filePath; + } + + /** + * getTypeArity returns the number of generic type parameters the given symbol + * has. If the symbol is not a type the result is null. + */ + getTypeArity(staticSymbol: StaticSymbol): number|null { + // If the file is a factory/ngsummary file, don't resolve the symbol as doing so would + // cause the metadata for an factory/ngsummary file to be loaded which doesn't exist. + // All references to generated classes must include the correct arity whenever + // generating code. + if (isGeneratedFile(staticSymbol.filePath)) { + return null; + } + let resolvedSymbol = this.resolveSymbol(staticSymbol); + while (resolvedSymbol && resolvedSymbol.metadata instanceof StaticSymbol) { + resolvedSymbol = this.resolveSymbol(resolvedSymbol.metadata); + } + return (resolvedSymbol && resolvedSymbol.metadata && resolvedSymbol.metadata.arity) || null; + } + + /** + * Converts a file path to a module name that can be used as an `import`. + */ + fileNameToModuleName(importedFilePath: string, containingFilePath: string): string|null { + return this.knownFileNameToModuleNames.get(importedFilePath) || + this.host.fileNameToModuleName(importedFilePath, containingFilePath); + } + + recordImportAs(sourceSymbol: StaticSymbol, targetSymbol: StaticSymbol) { + sourceSymbol.assertNoMembers(); + targetSymbol.assertNoMembers(); + this.importAs.set(sourceSymbol, targetSymbol); + } + + recordModuleNameForFileName(fileName: string, moduleName: string) { + this.knownFileNameToModuleNames.set(fileName, moduleName); + } + + /** + * Invalidate all information derived from the given file. + * + * @param fileName the file to invalidate + */ + invalidateFile(fileName: string) { + this.metadataCache.delete(fileName); + this.resolvedFilePaths.delete(fileName); + const symbols = this.symbolFromFile.get(fileName); + if (symbols) { + this.symbolFromFile.delete(fileName); + for (const symbol of symbols) { + this.resolvedSymbols.delete(symbol); + this.importAs.delete(symbol); + this.symbolResourcePaths.delete(symbol); + } + } + } + + /* @internal */ + ignoreErrorsFor(cb: () => T) { + const recorder = this.errorRecorder; + this.errorRecorder = () => {}; + try { + return cb(); + } finally { + this.errorRecorder = recorder; + } + } + + private _resolveSymbolMembers(staticSymbol: StaticSymbol): ResolvedStaticSymbol|null { + const members = staticSymbol.members; + const baseResolvedSymbol = + this.resolveSymbol(this.getStaticSymbol(staticSymbol.filePath, staticSymbol.name)); + if (!baseResolvedSymbol) { + return null; + } + const baseMetadata = baseResolvedSymbol.metadata; + if (baseMetadata instanceof StaticSymbol) { + return new ResolvedStaticSymbol( + staticSymbol, this.getStaticSymbol(baseMetadata.filePath, baseMetadata.name, members)); + } else if (baseMetadata && baseMetadata.__symbolic === 'class') { + if (baseMetadata.statics && members.length === 1) { + return new ResolvedStaticSymbol(staticSymbol, baseMetadata.statics[members[0]]); + } + } else { + let value = baseMetadata; + for (let i = 0; i < members.length && value; i++) { + value = value[members[i]]; + } + return new ResolvedStaticSymbol(staticSymbol, value); + } + return null; + } + + private _resolveSymbolFromSummary(staticSymbol: StaticSymbol): ResolvedStaticSymbol|null { + const summary = this.summaryResolver.resolveSummary(staticSymbol); + return summary ? new ResolvedStaticSymbol(staticSymbol, summary.metadata) : null; + } + + /** + * getStaticSymbol produces a Type whose metadata is known but whose implementation is not loaded. + * All types passed to the StaticResolver should be pseudo-types returned by this method. + * + * @param declarationFile the absolute path of the file where the symbol is declared + * @param name the name of the type. + * @param members a symbol for a static member of the named type + */ + getStaticSymbol(declarationFile: string, name: string, members?: string[]): StaticSymbol { + return this.staticSymbolCache.get(declarationFile, name, members); + } + + getSymbolsOf(filePath: string): StaticSymbol[] { + // Note: Some users use libraries that were not compiled with ngc, i.e. they don't + // have summaries, only .d.ts files. So we always need to check both, the summary + // and metadata. + let symbols = new Set(this.summaryResolver.getSymbolsOf(filePath)); + this._createSymbolsOf(filePath); + this.resolvedSymbols.forEach((resolvedSymbol) => { + if (resolvedSymbol.symbol.filePath === filePath) { + symbols.add(resolvedSymbol.symbol); + } + }); + return Array.from(symbols); + } + + private _createSymbolsOf(filePath: string) { + if (this.resolvedFilePaths.has(filePath)) { + return; + } + this.resolvedFilePaths.add(filePath); + const resolvedSymbols: ResolvedStaticSymbol[] = []; + const metadata = this.getModuleMetadata(filePath); + if (metadata['importAs']) { + // Index bundle indices should use the importAs module name defined + // in the bundle. + this.knownFileNameToModuleNames.set(filePath, metadata['importAs']); + } + if (metadata['metadata']) { + // handle direct declarations of the symbol + const topLevelSymbolNames = + new Set(Object.keys(metadata['metadata']).map(unescapeIdentifier)); + const origins: {[index: string]: string} = metadata['origins'] || {}; + Object.keys(metadata['metadata']).forEach((metadataKey) => { + const symbolMeta = metadata['metadata'][metadataKey]; + const name = unescapeIdentifier(metadataKey); + + const symbol = this.getStaticSymbol(filePath, name); + + const origin = origins.hasOwnProperty(metadataKey) && origins[metadataKey]; + if (origin) { + // If the symbol is from a bundled index, use the declaration location of the + // symbol so relative references (such as './my.html') will be calculated + // correctly. + const originFilePath = this.resolveModule(origin, filePath); + if (!originFilePath) { + this.reportError( + new Error(`Couldn't resolve original symbol for ${origin} from ${filePath}`)); + } else { + this.symbolResourcePaths.set(symbol, originFilePath); + } + } + resolvedSymbols.push( + this.createResolvedSymbol(symbol, filePath, topLevelSymbolNames, symbolMeta)); + }); + } + + // handle the symbols in one of the re-export location + if (metadata['exports']) { + for (const moduleExport of metadata['exports']) { + // handle the symbols in the list of explicitly re-exported symbols. + if (moduleExport.export) { + moduleExport.export.forEach((exportSymbol: any) => { + let symbolName: string; + if (typeof exportSymbol === 'string') { + symbolName = exportSymbol; + } else { + symbolName = exportSymbol.as; + } + symbolName = unescapeIdentifier(symbolName); + let symName = symbolName; + if (typeof exportSymbol !== 'string') { + symName = unescapeIdentifier(exportSymbol.name); + } + const resolvedModule = this.resolveModule(moduleExport.from, filePath); + if (resolvedModule) { + const targetSymbol = this.getStaticSymbol(resolvedModule, symName); + const sourceSymbol = this.getStaticSymbol(filePath, symbolName); + resolvedSymbols.push(this.createExport(sourceSymbol, targetSymbol)); + } + }); + } else { + // handle the symbols via export * directives. + const resolvedModule = this.resolveModule(moduleExport.from, filePath); + if (resolvedModule) { + const nestedExports = this.getSymbolsOf(resolvedModule); + nestedExports.forEach((targetSymbol) => { + const sourceSymbol = this.getStaticSymbol(filePath, targetSymbol.name); + resolvedSymbols.push(this.createExport(sourceSymbol, targetSymbol)); + }); + } + } + } + } + resolvedSymbols.forEach( + (resolvedSymbol) => this.resolvedSymbols.set(resolvedSymbol.symbol, resolvedSymbol)); + this.symbolFromFile.set(filePath, resolvedSymbols.map(resolvedSymbol => resolvedSymbol.symbol)); + } + + private createResolvedSymbol( + sourceSymbol: StaticSymbol, topLevelPath: string, topLevelSymbolNames: Set, + metadata: any): ResolvedStaticSymbol { + // For classes that don't have Angular summaries / metadata, + // we only keep their arity, but nothing else + // (e.g. their constructor parameters). + // We do this to prevent introducing deep imports + // as we didn't generate .ngfactory.ts files with proper reexports. + if (this.summaryResolver.isLibraryFile(sourceSymbol.filePath) && metadata && + metadata['__symbolic'] === 'class') { + const transformedMeta = {__symbolic: 'class', arity: metadata.arity}; + return new ResolvedStaticSymbol(sourceSymbol, transformedMeta); + } + + const self = this; + + class ReferenceTransformer extends ValueTransformer { + visitStringMap(map: {[key: string]: any}, functionParams: string[]): any { + const symbolic = map['__symbolic']; + if (symbolic === 'function') { + const oldLen = functionParams.length; + functionParams.push(...(map['parameters'] || [])); + const result = super.visitStringMap(map, functionParams); + functionParams.length = oldLen; + return result; + } else if (symbolic === 'reference') { + const module = map['module']; + const name = map['name'] ? unescapeIdentifier(map['name']) : map['name']; + if (!name) { + return null; + } + let filePath: string; + if (module) { + filePath = self.resolveModule(module, sourceSymbol.filePath) !; + if (!filePath) { + return { + __symbolic: 'error', + message: `Could not resolve ${module} relative to ${sourceSymbol.filePath}.` + }; + } + return self.getStaticSymbol(filePath, name); + } else if (functionParams.indexOf(name) >= 0) { + // reference to a function parameter + return {__symbolic: 'reference', name: name}; + } else { + if (topLevelSymbolNames.has(name)) { + return self.getStaticSymbol(topLevelPath, name); + } + // ambient value + null; + } + } else { + return super.visitStringMap(map, functionParams); + } + } + } + const transformedMeta = visitValue(metadata, new ReferenceTransformer(), []); + if (transformedMeta instanceof StaticSymbol) { + return this.createExport(sourceSymbol, transformedMeta); + } + return new ResolvedStaticSymbol(sourceSymbol, transformedMeta); + } + + private createExport(sourceSymbol: StaticSymbol, targetSymbol: StaticSymbol): + ResolvedStaticSymbol { + sourceSymbol.assertNoMembers(); + targetSymbol.assertNoMembers(); + if (this.summaryResolver.isLibraryFile(sourceSymbol.filePath)) { + // This case is for an ng library importing symbols from a plain ts library + // transitively. + // Note: We rely on the fact that we discover symbols in the direction + // from source files to library files + this.importAs.set(targetSymbol, this.getImportAs(sourceSymbol) || sourceSymbol); + } + return new ResolvedStaticSymbol(sourceSymbol, targetSymbol); + } + + private reportError(error: Error, context?: StaticSymbol, path?: string) { + if (this.errorRecorder) { + this.errorRecorder(error, (context && context.filePath) || path); + } else { + throw error; + } + } + + /** + * @param module an absolute path to a module file. + */ + private getModuleMetadata(module: string): {[key: string]: any} { + let moduleMetadata = this.metadataCache.get(module); + if (!moduleMetadata) { + const moduleMetadatas = this.host.getMetadataFor(module); + if (moduleMetadatas) { + let maxVersion = -1; + moduleMetadatas.forEach((md) => { + if (md['version'] > maxVersion) { + maxVersion = md['version']; + moduleMetadata = md; + } + }); + } + if (!moduleMetadata) { + moduleMetadata = + {__symbolic: 'module', version: SUPPORTED_SCHEMA_VERSION, module: module, metadata: {}}; + } + if (moduleMetadata['version'] != SUPPORTED_SCHEMA_VERSION) { + const errorMessage = moduleMetadata['version'] == 2 ? + `Unsupported metadata version ${moduleMetadata['version']} for module ${module}. This module should be compiled with a newer version of ngc` : + `Metadata version mismatch for module ${module}, found version ${moduleMetadata['version']}, expected ${SUPPORTED_SCHEMA_VERSION}`; + this.reportError(new Error(errorMessage)); + } + this.metadataCache.set(module, moduleMetadata); + } + return moduleMetadata; + } + + + getSymbolByModule(module: string, symbolName: string, containingFile?: string): StaticSymbol { + const filePath = this.resolveModule(module, containingFile); + if (!filePath) { + this.reportError( + new Error(`Could not resolve module ${module}${containingFile ? ` relative to $ { + containingFile + } `: ''}`)); + return this.getStaticSymbol(`ERROR:${module}`, symbolName); + } + return this.getStaticSymbol(filePath, symbolName); + } + + private resolveModule(module: string, containingFile?: string): string|null { + try { + return this.host.moduleNameToFileName(module, containingFile); + } catch (e) { + console.error(`Could not resolve module '${module}' relative to file ${containingFile}`); + this.reportError(e, undefined, containingFile); + } + return null; + } +} + +// Remove extra underscore from escaped identifier. +// See https://github.com/Microsoft/TypeScript/blob/master/src/compiler/utilities.ts +export function unescapeIdentifier(identifier: string): string { + return identifier.startsWith('___') ? identifier.substr(1) : identifier; +} diff --git a/angular/compiler/src/aot/summary_resolver.ts b/angular/compiler/src/aot/summary_resolver.ts new file mode 100644 index 0000000..c630675 --- /dev/null +++ b/angular/compiler/src/aot/summary_resolver.ts @@ -0,0 +1,114 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Summary, SummaryResolver} from '../summary_resolver'; + +import {StaticSymbol, StaticSymbolCache} from './static_symbol'; +import {deserializeSummaries} from './summary_serializer'; +import {ngfactoryFilePath, stripGeneratedFileSuffix, summaryFileName} from './util'; + +export interface AotSummaryResolverHost { + /** + * Loads an NgModule/Directive/Pipe summary file + */ + loadSummary(filePath: string): string|null; + + /** + * Returns whether a file is a source file or not. + */ + isSourceFile(sourceFilePath: string): boolean; + /** + * Converts a file name into a representation that should be stored in a summary file. + * This has to include changing the suffix as well. + * E.g. + * `some_file.ts` -> `some_file.d.ts` + * + * @param referringSrcFileName the soure file that refers to fileName + */ + toSummaryFileName(fileName: string, referringSrcFileName: string): string; + + /** + * Converts a fileName that was processed by `toSummaryFileName` back into a real fileName + * given the fileName of the library that is referrig to it. + */ + fromSummaryFileName(fileName: string, referringLibFileName: string): string; +} + +export class AotSummaryResolver implements SummaryResolver { + // Note: this will only contain StaticSymbols without members! + private summaryCache = new Map>(); + private loadedFilePaths = new Set(); + // Note: this will only contain StaticSymbols without members! + private importAs = new Map(); + + constructor(private host: AotSummaryResolverHost, private staticSymbolCache: StaticSymbolCache) {} + + isLibraryFile(filePath: string): boolean { + // Note: We need to strip the .ngfactory. file path, + // so this method also works for generated files + // (for which host.isSourceFile will always return false). + return !this.host.isSourceFile(stripGeneratedFileSuffix(filePath)); + } + + toSummaryFileName(filePath: string, referringSrcFileName: string) { + return this.host.toSummaryFileName(filePath, referringSrcFileName); + } + + fromSummaryFileName(fileName: string, referringLibFileName: string) { + return this.host.fromSummaryFileName(fileName, referringLibFileName); + } + + resolveSummary(staticSymbol: StaticSymbol): Summary { + staticSymbol.assertNoMembers(); + let summary = this.summaryCache.get(staticSymbol); + if (!summary) { + this._loadSummaryFile(staticSymbol.filePath); + summary = this.summaryCache.get(staticSymbol) !; + } + return summary; + } + + getSymbolsOf(filePath: string): StaticSymbol[] { + this._loadSummaryFile(filePath); + return Array.from(this.summaryCache.keys()).filter((symbol) => symbol.filePath === filePath); + } + + getImportAs(staticSymbol: StaticSymbol): StaticSymbol { + staticSymbol.assertNoMembers(); + return this.importAs.get(staticSymbol) !; + } + + addSummary(summary: Summary) { this.summaryCache.set(summary.symbol, summary); } + + private _loadSummaryFile(filePath: string) { + if (this.loadedFilePaths.has(filePath)) { + return; + } + this.loadedFilePaths.add(filePath); + if (this.isLibraryFile(filePath)) { + const summaryFilePath = summaryFileName(filePath); + let json: string|null; + try { + json = this.host.loadSummary(summaryFilePath); + } catch (e) { + console.error(`Error loading summary file ${summaryFilePath}`); + throw e; + } + if (json) { + const {summaries, importAs} = + deserializeSummaries(this.staticSymbolCache, this, filePath, json); + summaries.forEach((summary) => this.summaryCache.set(summary.symbol, summary)); + importAs.forEach((importAs) => { + this.importAs.set( + importAs.symbol, + this.staticSymbolCache.get(ngfactoryFilePath(filePath), importAs.importAs)); + }); + } + } + } +} diff --git a/angular/compiler/src/aot/summary_serializer.ts b/angular/compiler/src/aot/summary_serializer.ts new file mode 100644 index 0000000..165c803 --- /dev/null +++ b/angular/compiler/src/aot/summary_serializer.ts @@ -0,0 +1,354 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileNgModuleMetadata, CompileNgModuleSummary, CompilePipeMetadata, CompileProviderMetadata, CompileSummaryKind, CompileTypeMetadata, CompileTypeSummary} from '../compile_metadata'; +import * as o from '../output/output_ast'; +import {Summary, SummaryResolver} from '../summary_resolver'; +import {OutputContext, ValueTransformer, ValueVisitor, visitValue} from '../util'; + +import {StaticSymbol, StaticSymbolCache} from './static_symbol'; +import {ResolvedStaticSymbol, StaticSymbolResolver} from './static_symbol_resolver'; +import {summaryForJitFileName, summaryForJitName} from './util'; + +export function serializeSummaries( + srcFileName: string, forJitCtx: OutputContext, summaryResolver: SummaryResolver, + symbolResolver: StaticSymbolResolver, symbols: ResolvedStaticSymbol[], types: { + summary: CompileTypeSummary, + metadata: CompileNgModuleMetadata | CompileDirectiveMetadata | CompilePipeMetadata | + CompileTypeMetadata + }[]): {json: string, exportAs: {symbol: StaticSymbol, exportAs: string}[]} { + const toJsonSerializer = new ToJsonSerializer(symbolResolver, summaryResolver); + const forJitSerializer = new ForJitSerializer(forJitCtx, symbolResolver); + + // for symbols, we use everything except for the class metadata itself + // (we keep the statics though), as the class metadata is contained in the + // CompileTypeSummary. + symbols.forEach( + (resolvedSymbol) => toJsonSerializer.addOrMergeSummary( + {symbol: resolvedSymbol.symbol, metadata: resolvedSymbol.metadata})); + // Add summaries that are referenced by the given symbols (transitively) + // Note: the serializer.symbols array might be growing while + // we execute the loop! + for (let processedIndex = 0; processedIndex < toJsonSerializer.symbols.length; processedIndex++) { + const symbol = toJsonSerializer.symbols[processedIndex]; + if (summaryResolver.isLibraryFile(symbol.filePath)) { + let summary = summaryResolver.resolveSummary(symbol); + if (!summary) { + // some symbols might originate from a plain typescript library + // that just exported .d.ts and .metadata.json files, i.e. where no summary + // files were created. + const resolvedSymbol = symbolResolver.resolveSymbol(symbol); + if (resolvedSymbol) { + summary = {symbol: resolvedSymbol.symbol, metadata: resolvedSymbol.metadata}; + } + } + if (summary) { + if (summary.type) { + forJitSerializer.addLibType(summary.type); + } + toJsonSerializer.addOrMergeSummary(summary); + } + } + } + + // Add type summaries. + // Note: We don't add the summaries of all referenced symbols as for the ResolvedSymbols, + // as the type summaries already contain the transitive data that they require + // (in a minimal way). + types.forEach(({summary, metadata}) => { + forJitSerializer.addSourceType(summary, metadata); + toJsonSerializer.addOrMergeSummary( + {symbol: summary.type.reference, metadata: null, type: summary}); + if (summary.summaryKind === CompileSummaryKind.NgModule) { + const ngModuleSummary = summary; + ngModuleSummary.exportedDirectives.concat(ngModuleSummary.exportedPipes).forEach((id) => { + const symbol: StaticSymbol = id.reference; + if (summaryResolver.isLibraryFile(symbol.filePath)) { + const summary = summaryResolver.resolveSummary(symbol); + if (summary) { + toJsonSerializer.addOrMergeSummary(summary); + } + } + }); + } + }); + const {json, exportAs} = toJsonSerializer.serialize(srcFileName); + forJitSerializer.serialize(exportAs); + return {json, exportAs}; +} + +export function deserializeSummaries( + symbolCache: StaticSymbolCache, summaryResolver: SummaryResolver, + libraryFileName: string, json: string): + {summaries: Summary[], importAs: {symbol: StaticSymbol, importAs: string}[]} { + const deserializer = new FromJsonDeserializer(symbolCache, summaryResolver); + return deserializer.deserialize(libraryFileName, json); +} + +export function createForJitStub(outputCtx: OutputContext, reference: StaticSymbol) { + return createSummaryForJitFunction(outputCtx, reference, o.NULL_EXPR); +} + +function createSummaryForJitFunction( + outputCtx: OutputContext, reference: StaticSymbol, value: o.Expression) { + const fnName = summaryForJitName(reference.name); + outputCtx.statements.push( + o.fn([], [new o.ReturnStatement(value)], new o.ArrayType(o.DYNAMIC_TYPE)).toDeclStmt(fnName, [ + o.StmtModifier.Final, o.StmtModifier.Exported + ])); +} + +class ToJsonSerializer extends ValueTransformer { + // Note: This only contains symbols without members. + symbols: StaticSymbol[] = []; + private indexBySymbol = new Map(); + // This now contains a `__symbol: number` in the place of + // StaticSymbols, but otherwise has the same shape as the original objects. + private processedSummaryBySymbol = new Map(); + private processedSummaries: any[] = []; + + constructor( + private symbolResolver: StaticSymbolResolver, + private summaryResolver: SummaryResolver) { + super(); + } + + addOrMergeSummary(summary: Summary) { + let symbolMeta = summary.metadata; + if (symbolMeta && symbolMeta.__symbolic === 'class') { + // For classes, we keep everything except their class decorators. + // We need to keep e.g. the ctor args, method names, method decorators + // so that the class can be extended in another compilation unit. + // We don't keep the class decorators as + // 1) they refer to data + // that should not cause a rebuild of downstream compilation units + // (e.g. inline templates of @Component, or @NgModule.declarations) + // 2) their data is already captured in TypeSummaries, e.g. DirectiveSummary. + const clone: {[key: string]: any} = {}; + Object.keys(symbolMeta).forEach((propName) => { + if (propName !== 'decorators') { + clone[propName] = symbolMeta[propName]; + } + }); + symbolMeta = clone; + } + + let processedSummary = this.processedSummaryBySymbol.get(summary.symbol); + if (!processedSummary) { + processedSummary = this.processValue({symbol: summary.symbol}); + this.processedSummaries.push(processedSummary); + this.processedSummaryBySymbol.set(summary.symbol, processedSummary); + } + // Note: == on purpose to compare with undefined! + if (processedSummary.metadata == null && symbolMeta != null) { + processedSummary.metadata = this.processValue(symbolMeta); + } + // Note: == on purpose to compare with undefined! + if (processedSummary.type == null && summary.type != null) { + processedSummary.type = this.processValue(summary.type); + } + } + + serialize(srcFileName: string): + {json: string, exportAs: {symbol: StaticSymbol, exportAs: string}[]} { + const exportAs: {symbol: StaticSymbol, exportAs: string}[] = []; + const json = JSON.stringify({ + summaries: this.processedSummaries, + symbols: this.symbols.map((symbol, index) => { + symbol.assertNoMembers(); + let importAs: string = undefined !; + if (this.summaryResolver.isLibraryFile(symbol.filePath)) { + importAs = `${symbol.name}_${index}`; + exportAs.push({symbol, exportAs: importAs}); + } + return { + __symbol: index, + name: symbol.name, + filePath: this.summaryResolver.toSummaryFileName(symbol.filePath, srcFileName), + importAs: importAs + }; + }) + }); + return {json, exportAs}; + } + + private processValue(value: any): any { return visitValue(value, this, null); } + + visitOther(value: any, context: any): any { + if (value instanceof StaticSymbol) { + const baseSymbol = this.symbolResolver.getStaticSymbol(value.filePath, value.name); + let index = this.indexBySymbol.get(baseSymbol); + // Note: == on purpose to compare with undefined! + if (index == null) { + index = this.indexBySymbol.size; + this.indexBySymbol.set(baseSymbol, index); + this.symbols.push(baseSymbol); + } + return {__symbol: index, members: value.members}; + } + } +} + +class ForJitSerializer { + private data = new Map(); + + constructor(private outputCtx: OutputContext, private symbolResolver: StaticSymbolResolver) {} + + addSourceType( + summary: CompileTypeSummary, metadata: CompileNgModuleMetadata|CompileDirectiveMetadata| + CompilePipeMetadata|CompileTypeMetadata) { + this.data.set(summary.type.reference, {summary, metadata, isLibrary: false}); + } + + addLibType(summary: CompileTypeSummary) { + this.data.set(summary.type.reference, {summary, metadata: null, isLibrary: true}); + } + + serialize(exportAs: {symbol: StaticSymbol, exportAs: string}[]): void { + const ngModuleSymbols = new Set(); + + Array.from(this.data.values()).forEach(({summary, metadata, isLibrary}) => { + if (summary.summaryKind === CompileSummaryKind.NgModule) { + // collect the symbols that refer to NgModule classes. + // Note: we can't just rely on `summary.type.summaryKind` to determine this as + // we don't add the summaries of all referenced symbols when we serialize type summaries. + // See serializeSummaries for details. + ngModuleSymbols.add(summary.type.reference); + const modSummary = summary; + modSummary.modules.forEach((mod) => { ngModuleSymbols.add(mod.reference); }); + } + if (!isLibrary) { + const fnName = summaryForJitName(summary.type.reference.name); + createSummaryForJitFunction( + this.outputCtx, summary.type.reference, + this.serializeSummaryWithDeps(summary, metadata !)); + } + }); + + exportAs.forEach((entry) => { + const symbol = entry.symbol; + if (ngModuleSymbols.has(symbol)) { + const jitExportAsName = summaryForJitName(entry.exportAs); + this.outputCtx.statements.push( + o.variable(jitExportAsName).set(this.serializeSummaryRef(symbol)).toDeclStmt(null, [ + o.StmtModifier.Exported + ])); + } + }); + } + + private serializeSummaryWithDeps( + summary: CompileTypeSummary, metadata: CompileNgModuleMetadata|CompileDirectiveMetadata| + CompilePipeMetadata|CompileTypeMetadata): o.Expression { + const expressions: o.Expression[] = [this.serializeSummary(summary)]; + let providers: CompileProviderMetadata[] = []; + if (metadata instanceof CompileNgModuleMetadata) { + expressions.push(... + // For directives / pipes, we only add the declared ones, + // and rely on transitively importing NgModules to get the transitive + // summaries. + metadata.declaredDirectives.concat(metadata.declaredPipes) + .map(type => type.reference) + // For modules, + // we also add the summaries for modules + // from libraries. + // This is ok as we produce reexports for all transitive modules. + .concat(metadata.transitiveModule.modules.map(type => type.reference) + .filter(ref => ref !== metadata.type.reference)) + .map((ref) => this.serializeSummaryRef(ref))); + // Note: We don't use `NgModuleSummary.providers`, as that one is transitive, + // and we already have transitive modules. + providers = metadata.providers; + } else if (summary.summaryKind === CompileSummaryKind.Directive) { + const dirSummary = summary; + providers = dirSummary.providers.concat(dirSummary.viewProviders); + } + // Note: We can't just refer to the `ngsummary.ts` files for `useClass` providers (as we do for + // declaredDirectives / declaredPipes), as we allow + // providers without ctor arguments to skip the `@Injectable` decorator, + // i.e. we didn't generate .ngsummary.ts files for these. + expressions.push( + ...providers.filter(provider => !!provider.useClass).map(provider => this.serializeSummary({ + summaryKind: CompileSummaryKind.Injectable, type: provider.useClass + } as CompileTypeSummary))); + return o.literalArr(expressions); + } + + private serializeSummaryRef(typeSymbol: StaticSymbol): o.Expression { + const jitImportedSymbol = this.symbolResolver.getStaticSymbol( + summaryForJitFileName(typeSymbol.filePath), summaryForJitName(typeSymbol.name)); + return this.outputCtx.importExpr(jitImportedSymbol); + } + + private serializeSummary(data: {[key: string]: any}): o.Expression { + const outputCtx = this.outputCtx; + + class Transformer implements ValueVisitor { + visitArray(arr: any[], context: any): any { + return o.literalArr(arr.map(entry => visitValue(entry, this, context))); + } + visitStringMap(map: {[key: string]: any}, context: any): any { + return new o.LiteralMapExpr(Object.keys(map).map( + (key) => new o.LiteralMapEntry(key, visitValue(map[key], this, context), false))); + } + visitPrimitive(value: any, context: any): any { return o.literal(value); } + visitOther(value: any, context: any): any { + if (value instanceof StaticSymbol) { + return outputCtx.importExpr(value); + } else { + throw new Error(`Illegal State: Encountered value ${value}`); + } + } + } + + return visitValue(data, new Transformer(), null); + } +} + +class FromJsonDeserializer extends ValueTransformer { + private symbols: StaticSymbol[]; + + constructor( + private symbolCache: StaticSymbolCache, + private summaryResolver: SummaryResolver) { + super(); + } + + deserialize(libraryFileName: string, json: string): + {summaries: Summary[], importAs: {symbol: StaticSymbol, importAs: string}[]} { + const data: {summaries: any[], symbols: any[]} = JSON.parse(json); + const importAs: {symbol: StaticSymbol, importAs: string}[] = []; + this.symbols = []; + data.symbols.forEach((serializedSymbol) => { + const symbol = this.symbolCache.get( + this.summaryResolver.fromSummaryFileName(serializedSymbol.filePath, libraryFileName), + serializedSymbol.name); + this.symbols.push(symbol); + if (serializedSymbol.importAs) { + importAs.push({symbol: symbol, importAs: serializedSymbol.importAs}); + } + }); + const summaries = visitValue(data.summaries, this, null); + return {summaries, importAs}; + } + + visitStringMap(map: {[key: string]: any}, context: any): any { + if ('__symbol' in map) { + const baseSymbol = this.symbols[map['__symbol']]; + const members = map['members']; + return members.length ? this.symbolCache.get(baseSymbol.filePath, baseSymbol.name, members) : + baseSymbol; + } else { + return super.visitStringMap(map, context); + } + } +} diff --git a/angular/compiler/src/aot/util.ts b/angular/compiler/src/aot/util.ts new file mode 100644 index 0000000..1d02da8 --- /dev/null +++ b/angular/compiler/src/aot/util.ts @@ -0,0 +1,62 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +const STRIP_SRC_FILE_SUFFIXES = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/; +const GENERATED_FILE = /\.ngfactory\.|\.ngsummary\./; +const GENERATED_MODULE = /\.ngfactory$|\.ngsummary$/; +const JIT_SUMMARY_FILE = /\.ngsummary\./; +const JIT_SUMMARY_NAME = /NgSummary$/; + +export function ngfactoryFilePath(filePath: string, forceSourceFile = false): string { + const urlWithSuffix = splitTypescriptSuffix(filePath, forceSourceFile); + return `${urlWithSuffix[0]}.ngfactory${urlWithSuffix[1]}`; +} + +export function stripGeneratedFileSuffix(filePath: string): string { + return filePath.replace(GENERATED_FILE, '.'); +} + +export function isGeneratedFile(filePath: string): boolean { + return GENERATED_FILE.test(filePath); +} + +export function splitTypescriptSuffix(path: string, forceSourceFile = false): string[] { + if (path.endsWith('.d.ts')) { + return [path.slice(0, -5), forceSourceFile ? '.ts' : '.d.ts']; + } + + const lastDot = path.lastIndexOf('.'); + + if (lastDot !== -1) { + return [path.substring(0, lastDot), path.substring(lastDot)]; + } + + return [path, '']; +} + +export function summaryFileName(fileName: string): string { + const fileNameWithoutSuffix = fileName.replace(STRIP_SRC_FILE_SUFFIXES, ''); + return `${fileNameWithoutSuffix}.ngsummary.json`; +} + +export function summaryForJitFileName(fileName: string, forceSourceFile = false): string { + const urlWithSuffix = splitTypescriptSuffix(stripGeneratedFileSuffix(fileName), forceSourceFile); + return `${urlWithSuffix[0]}.ngsummary${urlWithSuffix[1]}`; +} + +export function stripSummaryForJitFileSuffix(filePath: string): string { + return filePath.replace(JIT_SUMMARY_FILE, '.'); +} + +export function summaryForJitName(symbolName: string): string { + return `${symbolName}NgSummary`; +} + +export function stripSummaryForJitNameSuffix(symbolName: string): string { + return symbolName.replace(JIT_SUMMARY_NAME, ''); +} \ No newline at end of file diff --git a/angular/compiler/src/assertions.ts b/angular/compiler/src/assertions.ts new file mode 100644 index 0000000..2b3736e --- /dev/null +++ b/angular/compiler/src/assertions.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export function assertArrayOfStrings(identifier: string, value: any) { + if (value == null) { + return; + } + if (!Array.isArray(value)) { + throw new Error(`Expected '${identifier}' to be an array of strings.`); + } + for (let i = 0; i < value.length; i += 1) { + if (typeof value[i] !== 'string') { + throw new Error(`Expected '${identifier}' to be an array of strings.`); + } + } +} + +const INTERPOLATION_BLACKLIST_REGEXPS = [ + /^\s*$/, // empty + /[<>]/, // html tag + /^[{}]$/, // i18n expansion + /&(#|[a-z])/i, // character reference, + /^\/\//, // comment +]; + +export function assertInterpolationSymbols(identifier: string, value: any): void { + if (value != null && !(Array.isArray(value) && value.length == 2)) { + throw new Error(`Expected '${identifier}' to be an array, [start, end].`); + } else if (value != null) { + const start = value[0] as string; + const end = value[1] as string; + // black list checking + INTERPOLATION_BLACKLIST_REGEXPS.forEach(regexp => { + if (regexp.test(start) || regexp.test(end)) { + throw new Error(`['${start}', '${end}'] contains unusable interpolation symbol.`); + } + }); + } +} diff --git a/angular/compiler/src/ast_path.ts b/angular/compiler/src/ast_path.ts new file mode 100644 index 0000000..b899bac --- /dev/null +++ b/angular/compiler/src/ast_path.ts @@ -0,0 +1,48 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * A path is an ordered set of elements. Typically a path is to a + * particular offset in a source file. The head of the list is the top + * most node. The tail is the node that contains the offset directly. + * + * For example, the expresion `a + b + c` might have an ast that looks + * like: + * + + * / \ + * a + + * / \ + * b c + * + * The path to the node at offset 9 would be `['+' at 1-10, '+' at 7-10, + * 'c' at 9-10]` and the path the node at offset 1 would be + * `['+' at 1-10, 'a' at 1-2]`. + */ +export class AstPath { + constructor(private path: T[], public position: number = -1) {} + + get empty(): boolean { return !this.path || !this.path.length; } + get head(): T|undefined { return this.path[0]; } + get tail(): T|undefined { return this.path[this.path.length - 1]; } + + parentOf(node: T|undefined): T|undefined { + return node && this.path[this.path.indexOf(node) - 1]; + } + childOf(node: T): T|undefined { return this.path[this.path.indexOf(node) + 1]; } + + first(ctor: {new (...args: any[]): N}): N|undefined { + for (let i = this.path.length - 1; i >= 0; i--) { + let item = this.path[i]; + if (item instanceof ctor) return item; + } + } + + push(node: T) { this.path.push(node); } + + pop(): T { return this.path.pop() !; } +} diff --git a/angular/compiler/src/chars.ts b/angular/compiler/src/chars.ts new file mode 100644 index 0000000..4e510a2 --- /dev/null +++ b/angular/compiler/src/chars.ts @@ -0,0 +1,89 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export const $EOF = 0; +export const $TAB = 9; +export const $LF = 10; +export const $VTAB = 11; +export const $FF = 12; +export const $CR = 13; +export const $SPACE = 32; +export const $BANG = 33; +export const $DQ = 34; +export const $HASH = 35; +export const $$ = 36; +export const $PERCENT = 37; +export const $AMPERSAND = 38; +export const $SQ = 39; +export const $LPAREN = 40; +export const $RPAREN = 41; +export const $STAR = 42; +export const $PLUS = 43; +export const $COMMA = 44; +export const $MINUS = 45; +export const $PERIOD = 46; +export const $SLASH = 47; +export const $COLON = 58; +export const $SEMICOLON = 59; +export const $LT = 60; +export const $EQ = 61; +export const $GT = 62; +export const $QUESTION = 63; + +export const $0 = 48; +export const $9 = 57; + +export const $A = 65; +export const $E = 69; +export const $F = 70; +export const $X = 88; +export const $Z = 90; + +export const $LBRACKET = 91; +export const $BACKSLASH = 92; +export const $RBRACKET = 93; +export const $CARET = 94; +export const $_ = 95; + +export const $a = 97; +export const $e = 101; +export const $f = 102; +export const $n = 110; +export const $r = 114; +export const $t = 116; +export const $u = 117; +export const $v = 118; +export const $x = 120; +export const $z = 122; + +export const $LBRACE = 123; +export const $BAR = 124; +export const $RBRACE = 125; +export const $NBSP = 160; + +export const $PIPE = 124; +export const $TILDA = 126; +export const $AT = 64; + +export const $BT = 96; + +export function isWhitespace(code: number): boolean { + return (code >= $TAB && code <= $SPACE) || (code == $NBSP); +} + +export function isDigit(code: number): boolean { + return $0 <= code && code <= $9; +} + +export function isAsciiLetter(code: number): boolean { + return code >= $a && code <= $z || code >= $A && code <= $Z; +} + +export function isAsciiHexDigit(code: number): boolean { + return code >= $a && code <= $f || code >= $A && code <= $F || isDigit(code); +} diff --git a/angular/compiler/src/compile_metadata.ts b/angular/compiler/src/compile_metadata.ts new file mode 100644 index 0000000..82829da --- /dev/null +++ b/angular/compiler/src/compile_metadata.ts @@ -0,0 +1,794 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {StaticSymbol} from './aot/static_symbol'; +import {ChangeDetectionStrategy, SchemaMetadata, Type, ViewEncapsulation} from './core'; +import {LifecycleHooks} from './lifecycle_reflector'; +import {CssSelector} from './selector'; +import {splitAtColon, stringify} from './util'; + + + +// group 0: "[prop] or (event) or @trigger" +// group 1: "prop" from "[prop]" +// group 2: "event" from "(event)" +// group 3: "@trigger" from "@trigger" +const HOST_REG_EXP = /^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\)))|(\@[-\w]+)$/; + +export class CompileAnimationEntryMetadata { + constructor( + public name: string|null = null, + public definitions: CompileAnimationStateMetadata[]|null = null) {} +} + +export abstract class CompileAnimationStateMetadata {} + +export class CompileAnimationStateDeclarationMetadata extends CompileAnimationStateMetadata { + constructor(public stateNameExpr: string, public styles: CompileAnimationStyleMetadata) { + super(); + } +} + +export class CompileAnimationStateTransitionMetadata extends CompileAnimationStateMetadata { + constructor( + public stateChangeExpr: string|StaticSymbol|((stateA: string, stateB: string) => boolean), + public steps: CompileAnimationMetadata) { + super(); + } +} + +export abstract class CompileAnimationMetadata {} + +export class CompileAnimationKeyframesSequenceMetadata extends CompileAnimationMetadata { + constructor(public steps: CompileAnimationStyleMetadata[] = []) { super(); } +} + +export class CompileAnimationStyleMetadata extends CompileAnimationMetadata { + constructor( + public offset: number, + public styles: Array|null = null) { + super(); + } +} + +export class CompileAnimationAnimateMetadata extends CompileAnimationMetadata { + constructor( + public timings: string|number = 0, public styles: CompileAnimationStyleMetadata| + CompileAnimationKeyframesSequenceMetadata|null = null) { + super(); + } +} + +export abstract class CompileAnimationWithStepsMetadata extends CompileAnimationMetadata { + constructor(public steps: CompileAnimationMetadata[]|null = null) { super(); } +} + +export class CompileAnimationSequenceMetadata extends CompileAnimationWithStepsMetadata { + constructor(steps: CompileAnimationMetadata[]|null = null) { super(steps); } +} + +export class CompileAnimationGroupMetadata extends CompileAnimationWithStepsMetadata { + constructor(steps: CompileAnimationMetadata[]|null = null) { super(steps); } +} + + +function _sanitizeIdentifier(name: string): string { + return name.replace(/\W/g, '_'); +} + +let _anonymousTypeIndex = 0; + +export function identifierName(compileIdentifier: CompileIdentifierMetadata | null | undefined): + string|null { + if (!compileIdentifier || !compileIdentifier.reference) { + return null; + } + const ref = compileIdentifier.reference; + if (ref instanceof StaticSymbol) { + return ref.name; + } + if (ref['__anonymousType']) { + return ref['__anonymousType']; + } + let identifier = stringify(ref); + if (identifier.indexOf('(') >= 0) { + // case: anonymous functions! + identifier = `anonymous_${_anonymousTypeIndex++}`; + ref['__anonymousType'] = identifier; + } else { + identifier = _sanitizeIdentifier(identifier); + } + return identifier; +} + +export function identifierModuleUrl(compileIdentifier: CompileIdentifierMetadata): string { + const ref = compileIdentifier.reference; + if (ref instanceof StaticSymbol) { + return ref.filePath; + } + // Runtime type + return `./${stringify(ref)}`; +} + +export function viewClassName(compType: any, embeddedTemplateIndex: number): string { + return `View_${identifierName({reference: compType})}_${embeddedTemplateIndex}`; +} + +export function rendererTypeName(compType: any): string { + return `RenderType_${identifierName({reference: compType})}`; +} + +export function hostViewClassName(compType: any): string { + return `HostView_${identifierName({reference: compType})}`; +} + +export function componentFactoryName(compType: any): string { + return `${identifierName({reference: compType})}NgFactory`; +} + +export interface ProxyClass { setDelegate(delegate: any): void; } + +export interface CompileIdentifierMetadata { reference: any; } + +export enum CompileSummaryKind { + Pipe, + Directive, + NgModule, + Injectable +} + +/** + * A CompileSummary is the data needed to use a directive / pipe / module + * in other modules / components. However, this data is not enough to compile + * the directive / module itself. + */ +export interface CompileTypeSummary { + summaryKind: CompileSummaryKind|null; + type: CompileTypeMetadata; +} + +export interface CompileDiDependencyMetadata { + isAttribute?: boolean; + isSelf?: boolean; + isHost?: boolean; + isSkipSelf?: boolean; + isOptional?: boolean; + isValue?: boolean; + token?: CompileTokenMetadata; + value?: any; +} + +export interface CompileProviderMetadata { + token: CompileTokenMetadata; + useClass?: CompileTypeMetadata; + useValue?: any; + useExisting?: CompileTokenMetadata; + useFactory?: CompileFactoryMetadata; + deps?: CompileDiDependencyMetadata[]; + multi?: boolean; +} + +export interface CompileFactoryMetadata extends CompileIdentifierMetadata { + diDeps: CompileDiDependencyMetadata[]; + reference: any; +} + +export function tokenName(token: CompileTokenMetadata) { + return token.value != null ? _sanitizeIdentifier(token.value) : identifierName(token.identifier); +} + +export function tokenReference(token: CompileTokenMetadata) { + if (token.identifier != null) { + return token.identifier.reference; + } else { + return token.value; + } +} + +export interface CompileTokenMetadata { + value?: any; + identifier?: CompileIdentifierMetadata|CompileTypeMetadata; +} + +/** + * Metadata regarding compilation of a type. + */ +export interface CompileTypeMetadata extends CompileIdentifierMetadata { + diDeps: CompileDiDependencyMetadata[]; + lifecycleHooks: LifecycleHooks[]; + reference: any; +} + +export interface CompileQueryMetadata { + selectors: Array; + descendants: boolean; + first: boolean; + propertyName: string; + read: CompileTokenMetadata; +} + +/** + * Metadata about a stylesheet + */ +export class CompileStylesheetMetadata { + moduleUrl: string|null; + styles: string[]; + styleUrls: string[]; + constructor( + {moduleUrl, styles, + styleUrls}: {moduleUrl?: string, styles?: string[], styleUrls?: string[]} = {}) { + this.moduleUrl = moduleUrl || null; + this.styles = _normalizeArray(styles); + this.styleUrls = _normalizeArray(styleUrls); + } +} + +/** + * Summary Metadata regarding compilation of a template. + */ +export interface CompileTemplateSummary { + animations: string[]|null; + ngContentSelectors: string[]; + encapsulation: ViewEncapsulation|null; +} + +/** + * Metadata regarding compilation of a template. + */ +export class CompileTemplateMetadata { + encapsulation: ViewEncapsulation|null; + template: string|null; + templateUrl: string|null; + isInline: boolean; + styles: string[]; + styleUrls: string[]; + externalStylesheets: CompileStylesheetMetadata[]; + animations: any[]; + ngContentSelectors: string[]; + interpolation: [string, string]|null; + preserveWhitespaces: boolean; + constructor({encapsulation, template, templateUrl, styles, styleUrls, externalStylesheets, + animations, ngContentSelectors, interpolation, isInline, preserveWhitespaces}: { + encapsulation: ViewEncapsulation | null, + template: string|null, + templateUrl: string|null, + styles: string[], + styleUrls: string[], + externalStylesheets: CompileStylesheetMetadata[], + ngContentSelectors: string[], + animations: any[], + interpolation: [string, string]|null, + isInline: boolean, + preserveWhitespaces: boolean + }) { + this.encapsulation = encapsulation; + this.template = template; + this.templateUrl = templateUrl; + this.styles = _normalizeArray(styles); + this.styleUrls = _normalizeArray(styleUrls); + this.externalStylesheets = _normalizeArray(externalStylesheets); + this.animations = animations ? flatten(animations) : []; + this.ngContentSelectors = ngContentSelectors || []; + if (interpolation && interpolation.length != 2) { + throw new Error(`'interpolation' should have a start and an end symbol.`); + } + this.interpolation = interpolation; + this.isInline = isInline; + this.preserveWhitespaces = preserveWhitespaces; + } + + toSummary(): CompileTemplateSummary { + return { + animations: this.animations.map(anim => anim.name), + ngContentSelectors: this.ngContentSelectors, + encapsulation: this.encapsulation, + }; + } +} + +export interface CompileEntryComponentMetadata { + componentType: any; + componentFactory: StaticSymbol|object; +} + +// Note: This should only use interfaces as nested data types +// as we need to be able to serialize this from/to JSON! +export interface CompileDirectiveSummary extends CompileTypeSummary { + type: CompileTypeMetadata; + isComponent: boolean; + selector: string|null; + exportAs: string|null; + inputs: {[key: string]: string}; + outputs: {[key: string]: string}; + hostListeners: {[key: string]: string}; + hostProperties: {[key: string]: string}; + hostAttributes: {[key: string]: string}; + providers: CompileProviderMetadata[]; + viewProviders: CompileProviderMetadata[]; + queries: CompileQueryMetadata[]; + viewQueries: CompileQueryMetadata[]; + entryComponents: CompileEntryComponentMetadata[]; + changeDetection: ChangeDetectionStrategy|null; + template: CompileTemplateSummary|null; + componentViewType: StaticSymbol|ProxyClass|null; + rendererType: StaticSymbol|object|null; + componentFactory: StaticSymbol|object|null; +} + +/** + * Metadata regarding compilation of a directive. + */ +export class CompileDirectiveMetadata { + static create({isHost, type, isComponent, selector, exportAs, changeDetection, inputs, outputs, + host, providers, viewProviders, queries, viewQueries, entryComponents, template, + componentViewType, rendererType, componentFactory}: { + isHost: boolean, + type: CompileTypeMetadata, + isComponent: boolean, + selector: string|null, + exportAs: string|null, + changeDetection: ChangeDetectionStrategy|null, + inputs: string[], + outputs: string[], + host: {[key: string]: string}, + providers: CompileProviderMetadata[], + viewProviders: CompileProviderMetadata[], + queries: CompileQueryMetadata[], + viewQueries: CompileQueryMetadata[], + entryComponents: CompileEntryComponentMetadata[], + template: CompileTemplateMetadata, + componentViewType: StaticSymbol|ProxyClass|null, + rendererType: StaticSymbol|object|null, + componentFactory: StaticSymbol|object|null, + }): CompileDirectiveMetadata { + const hostListeners: {[key: string]: string} = {}; + const hostProperties: {[key: string]: string} = {}; + const hostAttributes: {[key: string]: string} = {}; + if (host != null) { + Object.keys(host).forEach(key => { + const value = host[key]; + const matches = key.match(HOST_REG_EXP); + if (matches === null) { + hostAttributes[key] = value; + } else if (matches[1] != null) { + hostProperties[matches[1]] = value; + } else if (matches[2] != null) { + hostListeners[matches[2]] = value; + } + }); + } + const inputsMap: {[key: string]: string} = {}; + if (inputs != null) { + inputs.forEach((bindConfig: string) => { + // canonical syntax: `dirProp: elProp` + // if there is no `:`, use dirProp = elProp + const parts = splitAtColon(bindConfig, [bindConfig, bindConfig]); + inputsMap[parts[0]] = parts[1]; + }); + } + const outputsMap: {[key: string]: string} = {}; + if (outputs != null) { + outputs.forEach((bindConfig: string) => { + // canonical syntax: `dirProp: elProp` + // if there is no `:`, use dirProp = elProp + const parts = splitAtColon(bindConfig, [bindConfig, bindConfig]); + outputsMap[parts[0]] = parts[1]; + }); + } + + return new CompileDirectiveMetadata({ + isHost, + type, + isComponent: !!isComponent, selector, exportAs, changeDetection, + inputs: inputsMap, + outputs: outputsMap, + hostListeners, + hostProperties, + hostAttributes, + providers, + viewProviders, + queries, + viewQueries, + entryComponents, + template, + componentViewType, + rendererType, + componentFactory, + }); + } + isHost: boolean; + type: CompileTypeMetadata; + isComponent: boolean; + selector: string|null; + exportAs: string|null; + changeDetection: ChangeDetectionStrategy|null; + inputs: {[key: string]: string}; + outputs: {[key: string]: string}; + hostListeners: {[key: string]: string}; + hostProperties: {[key: string]: string}; + hostAttributes: {[key: string]: string}; + providers: CompileProviderMetadata[]; + viewProviders: CompileProviderMetadata[]; + queries: CompileQueryMetadata[]; + viewQueries: CompileQueryMetadata[]; + entryComponents: CompileEntryComponentMetadata[]; + + template: CompileTemplateMetadata|null; + + componentViewType: StaticSymbol|ProxyClass|null; + rendererType: StaticSymbol|object|null; + componentFactory: StaticSymbol|object|null; + + constructor({isHost, type, isComponent, selector, exportAs, + changeDetection, inputs, outputs, hostListeners, hostProperties, + hostAttributes, providers, viewProviders, queries, viewQueries, + entryComponents, template, componentViewType, rendererType, componentFactory}: { + isHost: boolean, + type: CompileTypeMetadata, + isComponent: boolean, + selector: string|null, + exportAs: string|null, + changeDetection: ChangeDetectionStrategy|null, + inputs: {[key: string]: string}, + outputs: {[key: string]: string}, + hostListeners: {[key: string]: string}, + hostProperties: {[key: string]: string}, + hostAttributes: {[key: string]: string}, + providers: CompileProviderMetadata[], + viewProviders: CompileProviderMetadata[], + queries: CompileQueryMetadata[], + viewQueries: CompileQueryMetadata[], + entryComponents: CompileEntryComponentMetadata[], + template: CompileTemplateMetadata|null, + componentViewType: StaticSymbol|ProxyClass|null, + rendererType: StaticSymbol|object|null, + componentFactory: StaticSymbol|object|null, + }) { + this.isHost = !!isHost; + this.type = type; + this.isComponent = isComponent; + this.selector = selector; + this.exportAs = exportAs; + this.changeDetection = changeDetection; + this.inputs = inputs; + this.outputs = outputs; + this.hostListeners = hostListeners; + this.hostProperties = hostProperties; + this.hostAttributes = hostAttributes; + this.providers = _normalizeArray(providers); + this.viewProviders = _normalizeArray(viewProviders); + this.queries = _normalizeArray(queries); + this.viewQueries = _normalizeArray(viewQueries); + this.entryComponents = _normalizeArray(entryComponents); + this.template = template; + + this.componentViewType = componentViewType; + this.rendererType = rendererType; + this.componentFactory = componentFactory; + } + + toSummary(): CompileDirectiveSummary { + return { + summaryKind: CompileSummaryKind.Directive, + type: this.type, + isComponent: this.isComponent, + selector: this.selector, + exportAs: this.exportAs, + inputs: this.inputs, + outputs: this.outputs, + hostListeners: this.hostListeners, + hostProperties: this.hostProperties, + hostAttributes: this.hostAttributes, + providers: this.providers, + viewProviders: this.viewProviders, + queries: this.queries, + viewQueries: this.viewQueries, + entryComponents: this.entryComponents, + changeDetection: this.changeDetection, + template: this.template && this.template.toSummary(), + componentViewType: this.componentViewType, + rendererType: this.rendererType, + componentFactory: this.componentFactory + }; + } +} + +/** + * Construct {@link CompileDirectiveMetadata} from {@link ComponentTypeMetadata} and a selector. + */ +export function createHostComponentMeta( + hostTypeReference: any, compMeta: CompileDirectiveMetadata, + hostViewType: StaticSymbol | ProxyClass): CompileDirectiveMetadata { + const template = CssSelector.parse(compMeta.selector !)[0].getMatchingElementTemplate(); + return CompileDirectiveMetadata.create({ + isHost: true, + type: {reference: hostTypeReference, diDeps: [], lifecycleHooks: []}, + template: new CompileTemplateMetadata({ + encapsulation: ViewEncapsulation.None, + template: template, + templateUrl: '', + styles: [], + styleUrls: [], + ngContentSelectors: [], + animations: [], + isInline: true, + externalStylesheets: [], + interpolation: null, + preserveWhitespaces: false, + }), + exportAs: null, + changeDetection: ChangeDetectionStrategy.Default, + inputs: [], + outputs: [], + host: {}, + isComponent: true, + selector: '*', + providers: [], + viewProviders: [], + queries: [], + viewQueries: [], + componentViewType: hostViewType, + rendererType: + {id: '__Host__', encapsulation: ViewEncapsulation.None, styles: [], data: {}} as object, + entryComponents: [], + componentFactory: null + }); +} + +export interface CompilePipeSummary extends CompileTypeSummary { + type: CompileTypeMetadata; + name: string; + pure: boolean; +} + +export class CompilePipeMetadata { + type: CompileTypeMetadata; + name: string; + pure: boolean; + + constructor({type, name, pure}: { + type: CompileTypeMetadata, + name: string, + pure: boolean, + }) { + this.type = type; + this.name = name; + this.pure = !!pure; + } + + toSummary(): CompilePipeSummary { + return { + summaryKind: CompileSummaryKind.Pipe, + type: this.type, + name: this.name, + pure: this.pure + }; + } +} + +// Note: This should only use interfaces as nested data types +// as we need to be able to serialize this from/to JSON! +export interface CompileNgModuleSummary extends CompileTypeSummary { + type: CompileTypeMetadata; + + // Note: This is transitive over the exported modules. + exportedDirectives: CompileIdentifierMetadata[]; + // Note: This is transitive over the exported modules. + exportedPipes: CompileIdentifierMetadata[]; + + // Note: This is transitive. + entryComponents: CompileEntryComponentMetadata[]; + // Note: This is transitive. + providers: {provider: CompileProviderMetadata, module: CompileIdentifierMetadata}[]; + // Note: This is transitive. + modules: CompileTypeMetadata[]; +} + +/** + * Metadata regarding compilation of a module. + */ +export class CompileNgModuleMetadata { + type: CompileTypeMetadata; + declaredDirectives: CompileIdentifierMetadata[]; + exportedDirectives: CompileIdentifierMetadata[]; + declaredPipes: CompileIdentifierMetadata[]; + + exportedPipes: CompileIdentifierMetadata[]; + entryComponents: CompileEntryComponentMetadata[]; + bootstrapComponents: CompileIdentifierMetadata[]; + providers: CompileProviderMetadata[]; + + importedModules: CompileNgModuleSummary[]; + exportedModules: CompileNgModuleSummary[]; + schemas: SchemaMetadata[]; + id: string|null; + + transitiveModule: TransitiveCompileNgModuleMetadata; + + constructor({type, providers, declaredDirectives, exportedDirectives, declaredPipes, + exportedPipes, entryComponents, bootstrapComponents, importedModules, + exportedModules, schemas, transitiveModule, id}: { + type: CompileTypeMetadata, + providers: CompileProviderMetadata[], + declaredDirectives: CompileIdentifierMetadata[], + exportedDirectives: CompileIdentifierMetadata[], + declaredPipes: CompileIdentifierMetadata[], + exportedPipes: CompileIdentifierMetadata[], + entryComponents: CompileEntryComponentMetadata[], + bootstrapComponents: CompileIdentifierMetadata[], + importedModules: CompileNgModuleSummary[], + exportedModules: CompileNgModuleSummary[], + transitiveModule: TransitiveCompileNgModuleMetadata, + schemas: SchemaMetadata[], + id: string|null + }) { + this.type = type || null; + this.declaredDirectives = _normalizeArray(declaredDirectives); + this.exportedDirectives = _normalizeArray(exportedDirectives); + this.declaredPipes = _normalizeArray(declaredPipes); + this.exportedPipes = _normalizeArray(exportedPipes); + this.providers = _normalizeArray(providers); + this.entryComponents = _normalizeArray(entryComponents); + this.bootstrapComponents = _normalizeArray(bootstrapComponents); + this.importedModules = _normalizeArray(importedModules); + this.exportedModules = _normalizeArray(exportedModules); + this.schemas = _normalizeArray(schemas); + this.id = id || null; + this.transitiveModule = transitiveModule || null; + } + + toSummary(): CompileNgModuleSummary { + const module = this.transitiveModule !; + return { + summaryKind: CompileSummaryKind.NgModule, + type: this.type, + entryComponents: module.entryComponents, + providers: module.providers, + modules: module.modules, + exportedDirectives: module.exportedDirectives, + exportedPipes: module.exportedPipes + }; + } +} + +export class TransitiveCompileNgModuleMetadata { + directivesSet = new Set(); + directives: CompileIdentifierMetadata[] = []; + exportedDirectivesSet = new Set(); + exportedDirectives: CompileIdentifierMetadata[] = []; + pipesSet = new Set(); + pipes: CompileIdentifierMetadata[] = []; + exportedPipesSet = new Set(); + exportedPipes: CompileIdentifierMetadata[] = []; + modulesSet = new Set(); + modules: CompileTypeMetadata[] = []; + entryComponentsSet = new Set(); + entryComponents: CompileEntryComponentMetadata[] = []; + + providers: {provider: CompileProviderMetadata, module: CompileIdentifierMetadata}[] = []; + + addProvider(provider: CompileProviderMetadata, module: CompileIdentifierMetadata) { + this.providers.push({provider: provider, module: module}); + } + + addDirective(id: CompileIdentifierMetadata) { + if (!this.directivesSet.has(id.reference)) { + this.directivesSet.add(id.reference); + this.directives.push(id); + } + } + addExportedDirective(id: CompileIdentifierMetadata) { + if (!this.exportedDirectivesSet.has(id.reference)) { + this.exportedDirectivesSet.add(id.reference); + this.exportedDirectives.push(id); + } + } + addPipe(id: CompileIdentifierMetadata) { + if (!this.pipesSet.has(id.reference)) { + this.pipesSet.add(id.reference); + this.pipes.push(id); + } + } + addExportedPipe(id: CompileIdentifierMetadata) { + if (!this.exportedPipesSet.has(id.reference)) { + this.exportedPipesSet.add(id.reference); + this.exportedPipes.push(id); + } + } + addModule(id: CompileTypeMetadata) { + if (!this.modulesSet.has(id.reference)) { + this.modulesSet.add(id.reference); + this.modules.push(id); + } + } + addEntryComponent(ec: CompileEntryComponentMetadata) { + if (!this.entryComponentsSet.has(ec.componentType)) { + this.entryComponentsSet.add(ec.componentType); + this.entryComponents.push(ec); + } + } +} + +function _normalizeArray(obj: any[] | undefined | null): any[] { + return obj || []; +} + +export class ProviderMeta { + token: any; + useClass: Type|null; + useValue: any; + useExisting: any; + useFactory: Function|null; + dependencies: Object[]|null; + multi: boolean; + + constructor(token: any, {useClass, useValue, useExisting, useFactory, deps, multi}: { + useClass?: Type, + useValue?: any, + useExisting?: any, + useFactory?: Function|null, + deps?: Object[]|null, + multi?: boolean + }) { + this.token = token; + this.useClass = useClass || null; + this.useValue = useValue; + this.useExisting = useExisting; + this.useFactory = useFactory || null; + this.dependencies = deps || null; + this.multi = !!multi; + } +} + +export function flatten(list: Array): T[] { + return list.reduce((flat: any[], item: T | T[]): T[] => { + const flatItem = Array.isArray(item) ? flatten(item) : item; + return (flat).concat(flatItem); + }, []); +} + +export function sourceUrl(url: string) { + // Note: We need 3 "/" so that ng shows up as a separate domain + // in the chrome dev tools. + return url.replace(/(\w+:\/\/[\w:-]+)?(\/+)?/, 'ng:///'); +} + +export function templateSourceUrl( + ngModuleType: CompileIdentifierMetadata, compMeta: {type: CompileIdentifierMetadata}, + templateMeta: {isInline: boolean, templateUrl: string | null}) { + let url: string; + if (templateMeta.isInline) { + if (compMeta.type.reference instanceof StaticSymbol) { + // Note: a .ts file might contain multiple components with inline templates, + // so we need to give them unique urls, as these will be used for sourcemaps. + url = `${compMeta.type.reference.filePath}.${compMeta.type.reference.name}.html`; + } else { + url = `${identifierName(ngModuleType)}/${identifierName(compMeta.type)}.html`; + } + } else { + url = templateMeta.templateUrl !; + } + // always prepend ng:// to make angular resources easy to find and not clobber + // user resources. + return sourceUrl(url); +} + +export function sharedStylesheetJitUrl(meta: CompileStylesheetMetadata, id: number) { + const pathParts = meta.moduleUrl !.split(/\/\\/g); + const baseName = pathParts[pathParts.length - 1]; + return sourceUrl(`css/${id}${baseName}.ngstyle.js`); +} + +export function ngModuleJitUrl(moduleMeta: CompileNgModuleMetadata): string { + return sourceUrl(`${identifierName(moduleMeta.type)}/module.ngfactory.js`); +} + +export function templateJitUrl( + ngModuleType: CompileIdentifierMetadata, compMeta: CompileDirectiveMetadata): string { + return sourceUrl(`${identifierName(ngModuleType)}/${identifierName(compMeta.type)}.ngfactory.js`); +} diff --git a/angular/compiler/src/compile_reflector.ts b/angular/compiler/src/compile_reflector.ts new file mode 100644 index 0000000..96cc30c --- /dev/null +++ b/angular/compiler/src/compile_reflector.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Component} from './core'; +import * as o from './output/output_ast'; + +/** + * Provides access to reflection data about symbols that the compiler needs. + */ +export abstract class CompileReflector { + abstract parameters(typeOrFunc: /*Type*/ any): any[][]; + abstract annotations(typeOrFunc: /*Type*/ any): any[]; + abstract propMetadata(typeOrFunc: /*Type*/ any): {[key: string]: any[]}; + abstract hasLifecycleHook(type: any, lcProperty: string): boolean; + abstract componentModuleUrl(type: /*Type*/ any, cmpMetadata: Component): string; + abstract resolveExternalReference(ref: o.ExternalReference): any; +} diff --git a/angular/compiler/src/compiler.ts b/angular/compiler/src/compiler.ts new file mode 100644 index 0000000..8b42c8e --- /dev/null +++ b/angular/compiler/src/compiler.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * @module + * @description + * Entry point for all APIs of the compiler package. + * + *
    + *
    Unstable APIs
    + *

    + * All compiler apis are currently considered experimental and private! + *

    + *

    + * We expect the APIs in this package to keep on changing. Do not rely on them. + *

    + *
    + */ + +import * as core from './core'; + +export {core}; + +export * from './version'; +export * from './template_parser/template_ast'; +export {CompilerConfig, preserveWhitespacesDefault} from './config'; +export * from './compile_metadata'; +export * from './aot/compiler_factory'; +export * from './aot/compiler'; +export * from './aot/generated_file'; +export * from './aot/compiler_options'; +export * from './aot/compiler_host'; +export * from './aot/static_reflector'; +export * from './aot/static_symbol'; +export * from './aot/static_symbol_resolver'; +export * from './aot/summary_resolver'; +export * from './ast_path'; +export * from './summary_resolver'; +export {Identifiers} from './identifiers'; +export {JitCompiler} from './jit/compiler'; +export * from './compile_reflector'; +export * from './url_resolver'; +export * from './resource_loader'; +export {DirectiveResolver} from './directive_resolver'; +export {PipeResolver} from './pipe_resolver'; +export {NgModuleResolver} from './ng_module_resolver'; +export {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './ml_parser/interpolation_config'; +export * from './schema/element_schema_registry'; +export * from './i18n/index'; +export * from './directive_normalizer'; +export * from './expression_parser/ast'; +export * from './expression_parser/lexer'; +export * from './expression_parser/parser'; +export * from './metadata_resolver'; +export * from './ml_parser/ast'; +export * from './ml_parser/html_parser'; +export * from './ml_parser/html_tags'; +export * from './ml_parser/interpolation_config'; +export * from './ml_parser/tags'; +export {NgModuleCompiler} from './ng_module_compiler'; +export {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement} from './output/output_ast'; +export {EmitterVisitorContext} from './output/abstract_emitter'; +export * from './output/ts_emitter'; +export * from './parse_util'; +export * from './schema/dom_element_schema_registry'; +export * from './selector'; +export * from './style_compiler'; +export * from './template_parser/template_parser'; +export {ViewCompiler} from './view_compiler/view_compiler'; +export {getParseErrors, isSyntaxError, syntaxError, Version} from './util'; +// This file only reexports content of the `src` folder. Keep it that way. diff --git a/angular/compiler/src/compiler_util/expression_converter.ts b/angular/compiler/src/compiler_util/expression_converter.ts new file mode 100644 index 0000000..a5abc8a --- /dev/null +++ b/angular/compiler/src/compiler_util/expression_converter.ts @@ -0,0 +1,622 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + + +import * as cdAst from '../expression_parser/ast'; +import {Identifiers} from '../identifiers'; +import * as o from '../output/output_ast'; + +export class EventHandlerVars { static event = o.variable('$event'); } + +export interface LocalResolver { getLocal(name: string): o.Expression|null; } + +export class ConvertActionBindingResult { + constructor(public stmts: o.Statement[], public allowDefault: o.ReadVarExpr) {} +} + +/** + * Converts the given expression AST into an executable output AST, assuming the expression is + * used in an action binding (e.g. an event handler). + */ +export function convertActionBinding( + localResolver: LocalResolver | null, implicitReceiver: o.Expression, action: cdAst.AST, + bindingId: string): ConvertActionBindingResult { + if (!localResolver) { + localResolver = new DefaultLocalResolver(); + } + const actionWithoutBuiltins = convertPropertyBindingBuiltins( + { + createLiteralArrayConverter: (argCount: number) => { + // Note: no caching for literal arrays in actions. + return (args: o.Expression[]) => o.literalArr(args); + }, + createLiteralMapConverter: (keys: {key: string, quoted: boolean}[]) => { + // Note: no caching for literal maps in actions. + return (values: o.Expression[]) => { + const entries = keys.map((k, i) => ({ + key: k.key, + value: values[i], + quoted: k.quoted, + })); + return o.literalMap(entries); + }; + }, + createPipeConverter: (name: string) => { + throw new Error(`Illegal State: Actions are not allowed to contain pipes. Pipe: ${name}`); + } + }, + action); + + const visitor = new _AstToIrVisitor(localResolver, implicitReceiver, bindingId); + const actionStmts: o.Statement[] = []; + flattenStatements(actionWithoutBuiltins.visit(visitor, _Mode.Statement), actionStmts); + prependTemporaryDecls(visitor.temporaryCount, bindingId, actionStmts); + const lastIndex = actionStmts.length - 1; + let preventDefaultVar: o.ReadVarExpr = null !; + if (lastIndex >= 0) { + const lastStatement = actionStmts[lastIndex]; + const returnExpr = convertStmtIntoExpression(lastStatement); + if (returnExpr) { + // Note: We need to cast the result of the method call to dynamic, + // as it might be a void method! + preventDefaultVar = createPreventDefaultVar(bindingId); + actionStmts[lastIndex] = + preventDefaultVar.set(returnExpr.cast(o.DYNAMIC_TYPE).notIdentical(o.literal(false))) + .toDeclStmt(null, [o.StmtModifier.Final]); + } + } + return new ConvertActionBindingResult(actionStmts, preventDefaultVar); +} + +export interface BuiltinConverter { (args: o.Expression[]): o.Expression; } + +export interface BuiltinConverterFactory { + createLiteralArrayConverter(argCount: number): BuiltinConverter; + createLiteralMapConverter(keys: {key: string, quoted: boolean}[]): BuiltinConverter; + createPipeConverter(name: string, argCount: number): BuiltinConverter; +} + +export function convertPropertyBindingBuiltins( + converterFactory: BuiltinConverterFactory, ast: cdAst.AST): cdAst.AST { + return convertBuiltins(converterFactory, ast); +} + +export class ConvertPropertyBindingResult { + constructor(public stmts: o.Statement[], public currValExpr: o.Expression) {} +} + +/** + * Converts the given expression AST into an executable output AST, assuming the expression + * is used in property binding. The expression has to be preprocessed via + * `convertPropertyBindingBuiltins`. + */ +export function convertPropertyBinding( + localResolver: LocalResolver | null, implicitReceiver: o.Expression, + expressionWithoutBuiltins: cdAst.AST, bindingId: string): ConvertPropertyBindingResult { + if (!localResolver) { + localResolver = new DefaultLocalResolver(); + } + const currValExpr = createCurrValueExpr(bindingId); + const stmts: o.Statement[] = []; + const visitor = new _AstToIrVisitor(localResolver, implicitReceiver, bindingId); + const outputExpr: o.Expression = expressionWithoutBuiltins.visit(visitor, _Mode.Expression); + + if (visitor.temporaryCount) { + for (let i = 0; i < visitor.temporaryCount; i++) { + stmts.push(temporaryDeclaration(bindingId, i)); + } + } + + stmts.push(currValExpr.set(outputExpr).toDeclStmt(null, [o.StmtModifier.Final])); + return new ConvertPropertyBindingResult(stmts, currValExpr); +} + +function convertBuiltins(converterFactory: BuiltinConverterFactory, ast: cdAst.AST): cdAst.AST { + const visitor = new _BuiltinAstConverter(converterFactory); + return ast.visit(visitor); +} + +function temporaryName(bindingId: string, temporaryNumber: number): string { + return `tmp_${bindingId}_${temporaryNumber}`; +} + +export function temporaryDeclaration(bindingId: string, temporaryNumber: number): o.Statement { + return new o.DeclareVarStmt(temporaryName(bindingId, temporaryNumber), o.NULL_EXPR); +} + +function prependTemporaryDecls( + temporaryCount: number, bindingId: string, statements: o.Statement[]) { + for (let i = temporaryCount - 1; i >= 0; i--) { + statements.unshift(temporaryDeclaration(bindingId, i)); + } +} + +enum _Mode { + Statement, + Expression +} + +function ensureStatementMode(mode: _Mode, ast: cdAst.AST) { + if (mode !== _Mode.Statement) { + throw new Error(`Expected a statement, but saw ${ast}`); + } +} + +function ensureExpressionMode(mode: _Mode, ast: cdAst.AST) { + if (mode !== _Mode.Expression) { + throw new Error(`Expected an expression, but saw ${ast}`); + } +} + +function convertToStatementIfNeeded(mode: _Mode, expr: o.Expression): o.Expression|o.Statement { + if (mode === _Mode.Statement) { + return expr.toStmt(); + } else { + return expr; + } +} + +class _BuiltinAstConverter extends cdAst.AstTransformer { + constructor(private _converterFactory: BuiltinConverterFactory) { super(); } + visitPipe(ast: cdAst.BindingPipe, context: any): any { + const args = [ast.exp, ...ast.args].map(ast => ast.visit(this, context)); + return new BuiltinFunctionCall( + ast.span, args, this._converterFactory.createPipeConverter(ast.name, args.length)); + } + visitLiteralArray(ast: cdAst.LiteralArray, context: any): any { + const args = ast.expressions.map(ast => ast.visit(this, context)); + return new BuiltinFunctionCall( + ast.span, args, this._converterFactory.createLiteralArrayConverter(ast.expressions.length)); + } + visitLiteralMap(ast: cdAst.LiteralMap, context: any): any { + const args = ast.values.map(ast => ast.visit(this, context)); + + return new BuiltinFunctionCall( + ast.span, args, this._converterFactory.createLiteralMapConverter(ast.keys)); + } +} + +class _AstToIrVisitor implements cdAst.AstVisitor { + private _nodeMap = new Map(); + private _resultMap = new Map(); + private _currentTemporary: number = 0; + public temporaryCount: number = 0; + + constructor( + private _localResolver: LocalResolver, private _implicitReceiver: o.Expression, + private bindingId: string) {} + + visitBinary(ast: cdAst.Binary, mode: _Mode): any { + let op: o.BinaryOperator; + switch (ast.operation) { + case '+': + op = o.BinaryOperator.Plus; + break; + case '-': + op = o.BinaryOperator.Minus; + break; + case '*': + op = o.BinaryOperator.Multiply; + break; + case '/': + op = o.BinaryOperator.Divide; + break; + case '%': + op = o.BinaryOperator.Modulo; + break; + case '&&': + op = o.BinaryOperator.And; + break; + case '||': + op = o.BinaryOperator.Or; + break; + case '==': + op = o.BinaryOperator.Equals; + break; + case '!=': + op = o.BinaryOperator.NotEquals; + break; + case '===': + op = o.BinaryOperator.Identical; + break; + case '!==': + op = o.BinaryOperator.NotIdentical; + break; + case '<': + op = o.BinaryOperator.Lower; + break; + case '>': + op = o.BinaryOperator.Bigger; + break; + case '<=': + op = o.BinaryOperator.LowerEquals; + break; + case '>=': + op = o.BinaryOperator.BiggerEquals; + break; + default: + throw new Error(`Unsupported operation ${ast.operation}`); + } + + return convertToStatementIfNeeded( + mode, + new o.BinaryOperatorExpr( + op, this._visit(ast.left, _Mode.Expression), this._visit(ast.right, _Mode.Expression))); + } + + visitChain(ast: cdAst.Chain, mode: _Mode): any { + ensureStatementMode(mode, ast); + return this.visitAll(ast.expressions, mode); + } + + visitConditional(ast: cdAst.Conditional, mode: _Mode): any { + const value: o.Expression = this._visit(ast.condition, _Mode.Expression); + return convertToStatementIfNeeded( + mode, value.conditional( + this._visit(ast.trueExp, _Mode.Expression), + this._visit(ast.falseExp, _Mode.Expression))); + } + + visitPipe(ast: cdAst.BindingPipe, mode: _Mode): any { + throw new Error( + `Illegal state: Pipes should have been converted into functions. Pipe: ${ast.name}`); + } + + visitFunctionCall(ast: cdAst.FunctionCall, mode: _Mode): any { + const convertedArgs = this.visitAll(ast.args, _Mode.Expression); + let fnResult: o.Expression; + if (ast instanceof BuiltinFunctionCall) { + fnResult = ast.converter(convertedArgs); + } else { + fnResult = this._visit(ast.target !, _Mode.Expression).callFn(convertedArgs); + } + return convertToStatementIfNeeded(mode, fnResult); + } + + visitImplicitReceiver(ast: cdAst.ImplicitReceiver, mode: _Mode): any { + ensureExpressionMode(mode, ast); + return this._implicitReceiver; + } + + visitInterpolation(ast: cdAst.Interpolation, mode: _Mode): any { + ensureExpressionMode(mode, ast); + const args = [o.literal(ast.expressions.length)]; + for (let i = 0; i < ast.strings.length - 1; i++) { + args.push(o.literal(ast.strings[i])); + args.push(this._visit(ast.expressions[i], _Mode.Expression)); + } + args.push(o.literal(ast.strings[ast.strings.length - 1])); + + return ast.expressions.length <= 9 ? + o.importExpr(Identifiers.inlineInterpolate).callFn(args) : + o.importExpr(Identifiers.interpolate).callFn([args[0], o.literalArr(args.slice(1))]); + } + + visitKeyedRead(ast: cdAst.KeyedRead, mode: _Mode): any { + const leftMostSafe = this.leftMostSafeNode(ast); + if (leftMostSafe) { + return this.convertSafeAccess(ast, leftMostSafe, mode); + } else { + return convertToStatementIfNeeded( + mode, this._visit(ast.obj, _Mode.Expression).key(this._visit(ast.key, _Mode.Expression))); + } + } + + visitKeyedWrite(ast: cdAst.KeyedWrite, mode: _Mode): any { + const obj: o.Expression = this._visit(ast.obj, _Mode.Expression); + const key: o.Expression = this._visit(ast.key, _Mode.Expression); + const value: o.Expression = this._visit(ast.value, _Mode.Expression); + return convertToStatementIfNeeded(mode, obj.key(key).set(value)); + } + + visitLiteralArray(ast: cdAst.LiteralArray, mode: _Mode): any { + throw new Error(`Illegal State: literal arrays should have been converted into functions`); + } + + visitLiteralMap(ast: cdAst.LiteralMap, mode: _Mode): any { + throw new Error(`Illegal State: literal maps should have been converted into functions`); + } + + visitLiteralPrimitive(ast: cdAst.LiteralPrimitive, mode: _Mode): any { + return convertToStatementIfNeeded(mode, o.literal(ast.value)); + } + + private _getLocal(name: string): o.Expression|null { return this._localResolver.getLocal(name); } + + visitMethodCall(ast: cdAst.MethodCall, mode: _Mode): any { + const leftMostSafe = this.leftMostSafeNode(ast); + if (leftMostSafe) { + return this.convertSafeAccess(ast, leftMostSafe, mode); + } else { + const args = this.visitAll(ast.args, _Mode.Expression); + let result: any = null; + const receiver = this._visit(ast.receiver, _Mode.Expression); + if (receiver === this._implicitReceiver) { + const varExpr = this._getLocal(ast.name); + if (varExpr) { + result = varExpr.callFn(args); + } + } + if (result == null) { + result = receiver.callMethod(ast.name, args); + } + return convertToStatementIfNeeded(mode, result); + } + } + + visitPrefixNot(ast: cdAst.PrefixNot, mode: _Mode): any { + return convertToStatementIfNeeded(mode, o.not(this._visit(ast.expression, _Mode.Expression))); + } + + visitNonNullAssert(ast: cdAst.NonNullAssert, mode: _Mode): any { + return convertToStatementIfNeeded( + mode, o.assertNotNull(this._visit(ast.expression, _Mode.Expression))); + } + + visitPropertyRead(ast: cdAst.PropertyRead, mode: _Mode): any { + const leftMostSafe = this.leftMostSafeNode(ast); + if (leftMostSafe) { + return this.convertSafeAccess(ast, leftMostSafe, mode); + } else { + let result: any = null; + const receiver = this._visit(ast.receiver, _Mode.Expression); + if (receiver === this._implicitReceiver) { + result = this._getLocal(ast.name); + } + if (result == null) { + result = receiver.prop(ast.name); + } + return convertToStatementIfNeeded(mode, result); + } + } + + visitPropertyWrite(ast: cdAst.PropertyWrite, mode: _Mode): any { + const receiver: o.Expression = this._visit(ast.receiver, _Mode.Expression); + if (receiver === this._implicitReceiver) { + const varExpr = this._getLocal(ast.name); + if (varExpr) { + throw new Error('Cannot assign to a reference or variable!'); + } + } + return convertToStatementIfNeeded( + mode, receiver.prop(ast.name).set(this._visit(ast.value, _Mode.Expression))); + } + + visitSafePropertyRead(ast: cdAst.SafePropertyRead, mode: _Mode): any { + return this.convertSafeAccess(ast, this.leftMostSafeNode(ast), mode); + } + + visitSafeMethodCall(ast: cdAst.SafeMethodCall, mode: _Mode): any { + return this.convertSafeAccess(ast, this.leftMostSafeNode(ast), mode); + } + + visitAll(asts: cdAst.AST[], mode: _Mode): any { return asts.map(ast => this._visit(ast, mode)); } + + visitQuote(ast: cdAst.Quote, mode: _Mode): any { + throw new Error(`Quotes are not supported for evaluation! + Statement: ${ast.uninterpretedExpression} located at ${ast.location}`); + } + + private _visit(ast: cdAst.AST, mode: _Mode): any { + const result = this._resultMap.get(ast); + if (result) return result; + return (this._nodeMap.get(ast) || ast).visit(this, mode); + } + + private convertSafeAccess( + ast: cdAst.AST, leftMostSafe: cdAst.SafeMethodCall|cdAst.SafePropertyRead, mode: _Mode): any { + // If the expression contains a safe access node on the left it needs to be converted to + // an expression that guards the access to the member by checking the receiver for blank. As + // execution proceeds from left to right, the left most part of the expression must be guarded + // first but, because member access is left associative, the right side of the expression is at + // the top of the AST. The desired result requires lifting a copy of the the left part of the + // expression up to test it for blank before generating the unguarded version. + + // Consider, for example the following expression: a?.b.c?.d.e + + // This results in the ast: + // . + // / \ + // ?. e + // / \ + // . d + // / \ + // ?. c + // / \ + // a b + + // The following tree should be generated: + // + // /---- ? ----\ + // / | \ + // a /--- ? ---\ null + // / | \ + // . . null + // / \ / \ + // . c . e + // / \ / \ + // a b , d + // / \ + // . c + // / \ + // a b + // + // Notice that the first guard condition is the left hand of the left most safe access node + // which comes in as leftMostSafe to this routine. + + let guardedExpression = this._visit(leftMostSafe.receiver, _Mode.Expression); + let temporary: o.ReadVarExpr = undefined !; + if (this.needsTemporary(leftMostSafe.receiver)) { + // If the expression has method calls or pipes then we need to save the result into a + // temporary variable to avoid calling stateful or impure code more than once. + temporary = this.allocateTemporary(); + + // Preserve the result in the temporary variable + guardedExpression = temporary.set(guardedExpression); + + // Ensure all further references to the guarded expression refer to the temporary instead. + this._resultMap.set(leftMostSafe.receiver, temporary); + } + const condition = guardedExpression.isBlank(); + + // Convert the ast to an unguarded access to the receiver's member. The map will substitute + // leftMostNode with its unguarded version in the call to `this.visit()`. + if (leftMostSafe instanceof cdAst.SafeMethodCall) { + this._nodeMap.set( + leftMostSafe, + new cdAst.MethodCall( + leftMostSafe.span, leftMostSafe.receiver, leftMostSafe.name, leftMostSafe.args)); + } else { + this._nodeMap.set( + leftMostSafe, + new cdAst.PropertyRead(leftMostSafe.span, leftMostSafe.receiver, leftMostSafe.name)); + } + + // Recursively convert the node now without the guarded member access. + const access = this._visit(ast, _Mode.Expression); + + // Remove the mapping. This is not strictly required as the converter only traverses each node + // once but is safer if the conversion is changed to traverse the nodes more than once. + this._nodeMap.delete(leftMostSafe); + + // If we allocated a temporary, release it. + if (temporary) { + this.releaseTemporary(temporary); + } + + // Produce the conditional + return convertToStatementIfNeeded(mode, condition.conditional(o.literal(null), access)); + } + + // Given a expression of the form a?.b.c?.d.e the the left most safe node is + // the (a?.b). The . and ?. are left associative thus can be rewritten as: + // ((((a?.c).b).c)?.d).e. This returns the most deeply nested safe read or + // safe method call as this needs be transform initially to: + // a == null ? null : a.c.b.c?.d.e + // then to: + // a == null ? null : a.b.c == null ? null : a.b.c.d.e + private leftMostSafeNode(ast: cdAst.AST): cdAst.SafePropertyRead|cdAst.SafeMethodCall { + const visit = (visitor: cdAst.AstVisitor, ast: cdAst.AST): any => { + return (this._nodeMap.get(ast) || ast).visit(visitor); + }; + return ast.visit({ + visitBinary(ast: cdAst.Binary) { return null; }, + visitChain(ast: cdAst.Chain) { return null; }, + visitConditional(ast: cdAst.Conditional) { return null; }, + visitFunctionCall(ast: cdAst.FunctionCall) { return null; }, + visitImplicitReceiver(ast: cdAst.ImplicitReceiver) { return null; }, + visitInterpolation(ast: cdAst.Interpolation) { return null; }, + visitKeyedRead(ast: cdAst.KeyedRead) { return visit(this, ast.obj); }, + visitKeyedWrite(ast: cdAst.KeyedWrite) { return null; }, + visitLiteralArray(ast: cdAst.LiteralArray) { return null; }, + visitLiteralMap(ast: cdAst.LiteralMap) { return null; }, + visitLiteralPrimitive(ast: cdAst.LiteralPrimitive) { return null; }, + visitMethodCall(ast: cdAst.MethodCall) { return visit(this, ast.receiver); }, + visitPipe(ast: cdAst.BindingPipe) { return null; }, + visitPrefixNot(ast: cdAst.PrefixNot) { return null; }, + visitNonNullAssert(ast: cdAst.NonNullAssert) { return null; }, + visitPropertyRead(ast: cdAst.PropertyRead) { return visit(this, ast.receiver); }, + visitPropertyWrite(ast: cdAst.PropertyWrite) { return null; }, + visitQuote(ast: cdAst.Quote) { return null; }, + visitSafeMethodCall(ast: cdAst.SafeMethodCall) { return visit(this, ast.receiver) || ast; }, + visitSafePropertyRead(ast: cdAst.SafePropertyRead) { + return visit(this, ast.receiver) || ast; + } + }); + } + + // Returns true of the AST includes a method or a pipe indicating that, if the + // expression is used as the target of a safe property or method access then + // the expression should be stored into a temporary variable. + private needsTemporary(ast: cdAst.AST): boolean { + const visit = (visitor: cdAst.AstVisitor, ast: cdAst.AST): boolean => { + return ast && (this._nodeMap.get(ast) || ast).visit(visitor); + }; + const visitSome = (visitor: cdAst.AstVisitor, ast: cdAst.AST[]): boolean => { + return ast.some(ast => visit(visitor, ast)); + }; + return ast.visit({ + visitBinary(ast: cdAst.Binary): + boolean{return visit(this, ast.left) || visit(this, ast.right);}, + visitChain(ast: cdAst.Chain) { return false; }, + visitConditional(ast: cdAst.Conditional): + boolean{return visit(this, ast.condition) || visit(this, ast.trueExp) || + visit(this, ast.falseExp);}, + visitFunctionCall(ast: cdAst.FunctionCall) { return true; }, + visitImplicitReceiver(ast: cdAst.ImplicitReceiver) { return false; }, + visitInterpolation(ast: cdAst.Interpolation) { return visitSome(this, ast.expressions); }, + visitKeyedRead(ast: cdAst.KeyedRead) { return false; }, + visitKeyedWrite(ast: cdAst.KeyedWrite) { return false; }, + visitLiteralArray(ast: cdAst.LiteralArray) { return true; }, + visitLiteralMap(ast: cdAst.LiteralMap) { return true; }, + visitLiteralPrimitive(ast: cdAst.LiteralPrimitive) { return false; }, + visitMethodCall(ast: cdAst.MethodCall) { return true; }, + visitPipe(ast: cdAst.BindingPipe) { return true; }, + visitPrefixNot(ast: cdAst.PrefixNot) { return visit(this, ast.expression); }, + visitNonNullAssert(ast: cdAst.PrefixNot) { return visit(this, ast.expression); }, + visitPropertyRead(ast: cdAst.PropertyRead) { return false; }, + visitPropertyWrite(ast: cdAst.PropertyWrite) { return false; }, + visitQuote(ast: cdAst.Quote) { return false; }, + visitSafeMethodCall(ast: cdAst.SafeMethodCall) { return true; }, + visitSafePropertyRead(ast: cdAst.SafePropertyRead) { return false; } + }); + } + + private allocateTemporary(): o.ReadVarExpr { + const tempNumber = this._currentTemporary++; + this.temporaryCount = Math.max(this._currentTemporary, this.temporaryCount); + return new o.ReadVarExpr(temporaryName(this.bindingId, tempNumber)); + } + + private releaseTemporary(temporary: o.ReadVarExpr) { + this._currentTemporary--; + if (temporary.name != temporaryName(this.bindingId, this._currentTemporary)) { + throw new Error(`Temporary ${temporary.name} released out of order`); + } + } +} + +function flattenStatements(arg: any, output: o.Statement[]) { + if (Array.isArray(arg)) { + (arg).forEach((entry) => flattenStatements(entry, output)); + } else { + output.push(arg); + } +} + +class DefaultLocalResolver implements LocalResolver { + getLocal(name: string): o.Expression|null { + if (name === EventHandlerVars.event.name) { + return EventHandlerVars.event; + } + return null; + } +} + +function createCurrValueExpr(bindingId: string): o.ReadVarExpr { + return o.variable(`currVal_${bindingId}`); // fix syntax highlighting: ` +} + +function createPreventDefaultVar(bindingId: string): o.ReadVarExpr { + return o.variable(`pd_${bindingId}`); +} + +function convertStmtIntoExpression(stmt: o.Statement): o.Expression|null { + if (stmt instanceof o.ExpressionStatement) { + return stmt.expr; + } else if (stmt instanceof o.ReturnStatement) { + return stmt.value; + } + return null; +} + +class BuiltinFunctionCall extends cdAst.FunctionCall { + constructor(span: cdAst.ParseSpan, public args: cdAst.AST[], public converter: BuiltinConverter) { + super(span, null, args); + } +} diff --git a/angular/compiler/src/config.ts b/angular/compiler/src/config.ts new file mode 100644 index 0000000..12bab10 --- /dev/null +++ b/angular/compiler/src/config.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {CompileIdentifierMetadata} from './compile_metadata'; +import {MissingTranslationStrategy, ViewEncapsulation} from './core'; +import {Identifiers} from './identifiers'; +import * as o from './output/output_ast'; +import {noUndefined} from './util'; + +export class CompilerConfig { + public defaultEncapsulation: ViewEncapsulation|null; + // Whether to support the `