From ebef5e697a19d38d761f15a123739481ddc74622 Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Tue, 25 Jul 2017 15:01:04 -0700 Subject: [PATCH 01/53] feat(forms): add options arg to abstract controls FormControls, FormGroups, and FormArrays now optionally accept an options object as their second argument. Validators and async validators can be passed in as part of this options object (though they can still be passed in as the second and third arg as before). ```ts const c = new FormControl(, { validators: [Validators.required], asyncValidators: [myAsyncValidator] }); ``` This commit also adds support for passing arrays of validators and async validators to FormGroups and FormArrays, which formerly only accepted individual functions. ```ts const g = new FormGroup({ one: new FormControl() }, [myPasswordValidator, myOtherValidator]); ``` This change paves the way for adding more options to AbstractControls, such as more fine-grained control of validation timing. --- packages/forms/src/model.ts | 104 +++++++++++++++---- packages/forms/test/form_array_spec.ts | 101 ++++++++++++++++++- packages/forms/test/form_control_spec.ts | 67 +++++++++++++ packages/forms/test/form_group_spec.ts | 121 +++++++++++++++++++---- tools/public_api_guard/forms/forms.d.ts | 6 +- 5 files changed, 353 insertions(+), 46 deletions(-) diff --git a/packages/forms/src/model.ts b/packages/forms/src/model.ts index 0f215ffbbb..1613e356b4 100644 --- a/packages/forms/src/model.ts +++ b/packages/forms/src/model.ts @@ -55,16 +55,41 @@ function _find(control: AbstractControl, path: Array| string, del }, control); } -function coerceToValidator(validator?: ValidatorFn | ValidatorFn[] | null): ValidatorFn|null { +function coerceToValidator( + validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null): ValidatorFn| + null { + const validator = + (isOptionsObj(validatorOrOpts) ? (validatorOrOpts as AbstractControlOptions).validators : + validatorOrOpts) as ValidatorFn | + ValidatorFn[] | null; + return Array.isArray(validator) ? composeValidators(validator) : validator || null; } -function coerceToAsyncValidator(asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): - AsyncValidatorFn|null { - return Array.isArray(asyncValidator) ? composeAsyncValidators(asyncValidator) : - asyncValidator || null; +function coerceToAsyncValidator( + asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null, validatorOrOpts?: ValidatorFn | + ValidatorFn[] | AbstractControlOptions | null): AsyncValidatorFn|null { + const origAsyncValidator = + (isOptionsObj(validatorOrOpts) ? (validatorOrOpts as AbstractControlOptions).asyncValidators : + asyncValidator) as AsyncValidatorFn | + AsyncValidatorFn | null; + + return Array.isArray(origAsyncValidator) ? composeAsyncValidators(origAsyncValidator) : + origAsyncValidator || null; } +export interface AbstractControlOptions { + validators?: ValidatorFn|ValidatorFn[]|null; + asyncValidators?: AsyncValidatorFn|AsyncValidatorFn[]|null; +} + +function isOptionsObj( + validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null): boolean { + return validatorOrOpts != null && !Array.isArray(validatorOrOpts) && + typeof validatorOrOpts === 'object'; +} + + /** * @whatItDoes This is the base class for {@link FormControl}, {@link FormGroup}, and * {@link FormArray}. @@ -612,9 +637,12 @@ export abstract class AbstractControl { * console.log(ctrl.status); // 'DISABLED' * ``` * - * To include a sync validator (or an array of sync validators) with the control, - * pass it in as the second argument. Async validators are also supported, but - * have to be passed in separately as the third arg. + * The second {@link FormControl} argument can accept one of three things: + * * a sync validator function + * * an array of sync validator functions + * * an options object containing validator and/or async validator functions + * + * Example of a single sync validator function: * * ```ts * const ctrl = new FormControl('', Validators.required); @@ -622,6 +650,15 @@ export abstract class AbstractControl { * console.log(ctrl.status); // 'INVALID' * ``` * + * Example using options object: + * + * ```ts + * const ctrl = new FormControl('', { + * validators: Validators.required, + * asyncValidators: myAsyncValidator + * }); + * ``` + * * See its superclass, {@link AbstractControl}, for more properties and methods. * * * **npm package**: `@angular/forms` @@ -633,9 +670,12 @@ export class FormControl extends AbstractControl { _onChange: Function[] = []; constructor( - formState: any = null, validator?: ValidatorFn|ValidatorFn[]|null, + formState: any = null, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { - super(coerceToValidator(validator), coerceToAsyncValidator(asyncValidator)); + super( + coerceToValidator(validatorOrOpts), + coerceToAsyncValidator(asyncValidator, validatorOrOpts)); this._applyFormState(formState); this.updateValueAndValidity({onlySelf: true, emitEvent: false}); this._initObservables(); @@ -823,15 +863,28 @@ export class FormControl extends AbstractControl { * } * ``` * + * Like {@link FormControl} instances, you can alternatively choose to pass in + * validators and async validators as part of an options object. + * + * ``` + * const form = new FormGroup({ + * password: new FormControl('') + * passwordConfirm: new FormControl('') + * }, {validators: passwordMatchValidator, asyncValidators: otherValidator}); + * ``` + * * * **npm package**: `@angular/forms` * * @stable */ export class FormGroup extends AbstractControl { constructor( - public controls: {[key: string]: AbstractControl}, validator?: ValidatorFn|null, - asyncValidator?: AsyncValidatorFn|null) { - super(validator || null, asyncValidator || null); + public controls: {[key: string]: AbstractControl}, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { + super( + coerceToValidator(validatorOrOpts), + coerceToAsyncValidator(asyncValidator, validatorOrOpts)); this._initObservables(); this._setUpControls(); this.updateValueAndValidity({onlySelf: true, emitEvent: false}); @@ -1114,9 +1167,19 @@ export class FormGroup extends AbstractControl { * console.log(arr.status); // 'VALID' * ``` * - * You can also include array-level validators as the second arg, or array-level async - * validators as the third arg. These come in handy when you want to perform validation - * that considers the value of more than one child control. + * You can also include array-level validators and async validators. These come in handy + * when you want to perform validation that considers the value of more than one child + * control. + * + * The two types of validators can be passed in separately as the second and third arg + * respectively, or together as part of an options object. + * + * ``` + * const arr = new FormArray([ + * new FormControl('Nancy'), + * new FormControl('Drew') + * ], {validators: myValidator, asyncValidators: myAsyncValidator}); + * ``` * * ### Adding or removing controls * @@ -1132,9 +1195,12 @@ export class FormGroup extends AbstractControl { */ export class FormArray extends AbstractControl { constructor( - public controls: AbstractControl[], validator?: ValidatorFn|null, - asyncValidator?: AsyncValidatorFn|null) { - super(validator || null, asyncValidator || null); + public controls: AbstractControl[], + validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { + super( + coerceToValidator(validatorOrOpts), + coerceToAsyncValidator(asyncValidator, validatorOrOpts)); this._initObservables(); this._setUpControls(); this.updateValueAndValidity({onlySelf: true, emitEvent: false}); diff --git a/packages/forms/test/form_array_spec.ts b/packages/forms/test/form_array_spec.ts index 2b94b80ec7..11b0605ae3 100644 --- a/packages/forms/test/form_array_spec.ts +++ b/packages/forms/test/form_array_spec.ts @@ -8,8 +8,8 @@ import {fakeAsync, tick} from '@angular/core/testing'; import {AsyncTestCompleter, beforeEach, describe, inject, it} from '@angular/core/testing/src/testing_internal'; -import {AbstractControl, FormArray, FormControl, FormGroup} from '@angular/forms'; - +import {AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors} from '@angular/forms'; +import {of } from 'rxjs/observable/of'; import {Validators} from '../src/validators'; export function main() { @@ -725,18 +725,113 @@ export function main() { }); }); + describe('validator', () => { + function simpleValidator(c: AbstractControl): ValidationErrors|null { + return c.get([0]) !.value === 'correct' ? null : {'broken': true}; + } + + function arrayRequiredValidator(c: AbstractControl): ValidationErrors|null { + return Validators.required(c.get([0]) as AbstractControl); + } + + it('should set a single validator', () => { + const a = new FormArray([new FormControl()], simpleValidator); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'broken': true}); + + a.setValue(['correct']); + expect(a.valid).toBe(true); + }); + + it('should set a single validator from options obj', () => { + const a = new FormArray([new FormControl()], {validators: simpleValidator}); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'broken': true}); + + a.setValue(['correct']); + expect(a.valid).toBe(true); + }); + + it('should set multiple validators from an array', () => { + const a = new FormArray([new FormControl()], [simpleValidator, arrayRequiredValidator]); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'required': true, 'broken': true}); + + a.setValue(['c']); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'broken': true}); + + a.setValue(['correct']); + expect(a.valid).toBe(true); + }); + + it('should set multiple validators from options obj', () => { + const a = new FormArray( + [new FormControl()], {validators: [simpleValidator, arrayRequiredValidator]}); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'required': true, 'broken': true}); + + a.setValue(['c']); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'broken': true}); + + a.setValue(['correct']); + expect(a.valid).toBe(true); + }); + }); + describe('asyncValidator', () => { + function otherObservableValidator() { return of ({'other': true}); } + it('should run the async validator', fakeAsync(() => { const c = new FormControl('value'); const g = new FormArray([c], null !, asyncValidator('expected')); expect(g.pending).toEqual(true); - tick(1); + tick(); expect(g.errors).toEqual({'async': true}); expect(g.pending).toEqual(false); })); + + it('should set a single async validator from options obj', fakeAsync(() => { + const g = new FormArray( + [new FormControl('value')], {asyncValidators: asyncValidator('expected')}); + + expect(g.pending).toEqual(true); + + tick(); + + expect(g.errors).toEqual({'async': true}); + expect(g.pending).toEqual(false); + })); + + it('should set multiple async validators from an array', fakeAsync(() => { + const g = new FormArray( + [new FormControl('value')], null !, + [asyncValidator('expected'), otherObservableValidator]); + + expect(g.pending).toEqual(true); + + tick(); + + expect(g.errors).toEqual({'async': true, 'other': true}); + expect(g.pending).toEqual(false); + })); + + it('should set multiple async validators from options obj', fakeAsync(() => { + const g = new FormArray( + [new FormControl('value')], + {asyncValidators: [asyncValidator('expected'), otherObservableValidator]}); + + expect(g.pending).toEqual(true); + + tick(); + + expect(g.errors).toEqual({'async': true, 'other': true}); + expect(g.pending).toEqual(false); + })); }); describe('disable() & enable()', () => { diff --git a/packages/forms/test/form_control_spec.ts b/packages/forms/test/form_control_spec.ts index 4fa4d9f3a2..5199aaac5b 100644 --- a/packages/forms/test/form_control_spec.ts +++ b/packages/forms/test/form_control_spec.ts @@ -97,6 +97,39 @@ export function main() { expect(c.valid).toEqual(true); }); + it('should support single validator from options obj', () => { + const c = new FormControl(null, {validators: Validators.required}); + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({required: true}); + + c.setValue('value'); + expect(c.valid).toEqual(true); + }); + + it('should support multiple validators from options obj', () => { + const c = + new FormControl(null, {validators: [Validators.required, Validators.minLength(3)]}); + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({required: true}); + + c.setValue('aa'); + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({minlength: {requiredLength: 3, actualLength: 2}}); + + c.setValue('aaa'); + expect(c.valid).toEqual(true); + }); + + it('should support a null validators value', () => { + const c = new FormControl(null, {validators: null}); + expect(c.valid).toEqual(true); + }); + + it('should support an empty options obj', () => { + const c = new FormControl(null, {}); + expect(c.valid).toEqual(true); + }); + it('should return errors', () => { const c = new FormControl(null, Validators.required); expect(c.errors).toEqual({'required': true}); @@ -222,6 +255,40 @@ export function main() { expect(c.errors).toEqual({'async': true, 'other': true}); })); + + it('should support a single async validator from options obj', fakeAsync(() => { + const c = new FormControl('value', {asyncValidators: asyncValidator('expected')}); + expect(c.pending).toEqual(true); + tick(); + + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({'async': true}); + })); + + it('should support multiple async validators from options obj', fakeAsync(() => { + const c = new FormControl( + 'value', {asyncValidators: [asyncValidator('expected'), otherAsyncValidator]}); + expect(c.pending).toEqual(true); + tick(); + + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({'async': true, 'other': true}); + })); + + it('should support a mix of validators from options obj', fakeAsync(() => { + const c = new FormControl( + '', {validators: Validators.required, asyncValidators: asyncValidator('expected')}); + tick(); + expect(c.errors).toEqual({required: true}); + + c.setValue('value'); + expect(c.pending).toBe(true); + + tick(); + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({'async': true}); + })); + it('should add single async validator', fakeAsync(() => { const c = new FormControl('value', null !); diff --git a/packages/forms/test/form_group_spec.ts b/packages/forms/test/form_group_spec.ts index aa62b78c9c..b9a1fbbf03 100644 --- a/packages/forms/test/form_group_spec.ts +++ b/packages/forms/test/form_group_spec.ts @@ -9,10 +9,15 @@ import {EventEmitter} from '@angular/core'; import {async, fakeAsync, tick} from '@angular/core/testing'; import {AsyncTestCompleter, beforeEach, describe, inject, it} from '@angular/core/testing/src/testing_internal'; -import {AbstractControl, FormArray, FormControl, FormGroup, Validators} from '@angular/forms'; +import {AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors, Validators} from '@angular/forms'; +import {of } from 'rxjs/observable/of'; export function main() { + function simpleValidator(c: AbstractControl): ValidationErrors|null { + return c.get('one') !.value === 'correct' ? null : {'broken': true}; + } + function asyncValidator(expected: string, timeouts = {}) { return (c: AbstractControl) => { let resolve: (result: any) => void = undefined !; @@ -36,6 +41,8 @@ export function main() { return e; } + function otherObservableValidator() { return of ({'other': true}) } + describe('FormGroup', () => { describe('value', () => { it('should be the reduced value of the child controls', () => { @@ -104,26 +111,6 @@ export function main() { }); }); - describe('errors', () => { - it('should run the validator when the value changes', () => { - const simpleValidator = (c: FormGroup) => - c.controls['one'].value != 'correct' ? {'broken': true} : null; - - const c = new FormControl(null); - const g = new FormGroup({'one': c}, simpleValidator); - - c.setValue('correct'); - - expect(g.valid).toEqual(true); - expect(g.errors).toEqual(null); - - c.setValue('incorrect'); - - expect(g.valid).toEqual(false); - expect(g.errors).toEqual({'broken': true}); - }); - }); - describe('dirty', () => { let c: FormControl, g: FormGroup; @@ -687,6 +674,66 @@ export function main() { }); }); + describe('validator', () => { + + function containsValidator(c: AbstractControl): ValidationErrors|null { + return c.get('one') !.value && c.get('one') !.value.indexOf('c') !== -1 ? null : + {'missing': true}; + } + + it('should run a single validator when the value changes', () => { + const c = new FormControl(null); + const g = new FormGroup({'one': c}, simpleValidator); + + c.setValue('correct'); + + expect(g.valid).toEqual(true); + expect(g.errors).toEqual(null); + + c.setValue('incorrect'); + + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({'broken': true}); + }); + + it('should support multiple validators from array', () => { + const g = new FormGroup({one: new FormControl()}, [simpleValidator, containsValidator]); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({missing: true, broken: true}); + + g.setValue({one: 'c'}); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({broken: true}); + + g.setValue({one: 'correct'}); + expect(g.valid).toEqual(true); + }); + + it('should set single validator from options obj', () => { + const g = new FormGroup({one: new FormControl()}, {validators: simpleValidator}); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({broken: true}); + + g.setValue({one: 'correct'}); + expect(g.valid).toEqual(true); + }); + + it('should set multiple validators from options obj', () => { + const g = new FormGroup( + {one: new FormControl()}, {validators: [simpleValidator, containsValidator]}); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({missing: true, broken: true}); + + g.setValue({one: 'c'}); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({broken: true}); + + g.setValue({one: 'correct'}); + expect(g.valid).toEqual(true); + }); + + }); + describe('asyncValidator', () => { it('should run the async validator', fakeAsync(() => { const c = new FormControl('value'); @@ -700,6 +747,38 @@ export function main() { expect(g.pending).toEqual(false); })); + it('should set multiple async validators from array', fakeAsync(() => { + const g = new FormGroup( + {'one': new FormControl('value')}, null !, + [asyncValidator('expected'), otherObservableValidator]); + expect(g.pending).toEqual(true); + + tick(); + expect(g.errors).toEqual({'async': true, 'other': true}); + expect(g.pending).toEqual(false); + })); + + it('should set single async validator from options obj', fakeAsync(() => { + const g = new FormGroup( + {'one': new FormControl('value')}, {asyncValidators: asyncValidator('expected')}); + expect(g.pending).toEqual(true); + + tick(); + expect(g.errors).toEqual({'async': true}); + expect(g.pending).toEqual(false); + })); + + it('should set multiple async validators from options obj', fakeAsync(() => { + const g = new FormGroup( + {'one': new FormControl('value')}, + {asyncValidators: [asyncValidator('expected'), otherObservableValidator]}); + expect(g.pending).toEqual(true); + + tick(); + expect(g.errors).toEqual({'async': true, 'other': true}); + expect(g.pending).toEqual(false); + })); + it('should set the parent group\'s status to pending', fakeAsync(() => { const c = new FormControl('value', null !, asyncValidator('expected')); const g = new FormGroup({'one': c}); diff --git a/tools/public_api_guard/forms/forms.d.ts b/tools/public_api_guard/forms/forms.d.ts index fcd570bc0a..85ebcb1b0d 100644 --- a/tools/public_api_guard/forms/forms.d.ts +++ b/tools/public_api_guard/forms/forms.d.ts @@ -175,7 +175,7 @@ export interface Form { export declare class FormArray extends AbstractControl { controls: AbstractControl[]; readonly length: number; - constructor(controls: AbstractControl[], validator?: ValidatorFn | null, asyncValidator?: AsyncValidatorFn | null); + constructor(controls: AbstractControl[], validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); at(index: number): AbstractControl; getRawValue(): any[]; insert(index: number, control: AbstractControl): void; @@ -222,7 +222,7 @@ export declare class FormBuilder { /** @stable */ export declare class FormControl extends AbstractControl { - constructor(formState?: any, validator?: ValidatorFn | ValidatorFn[] | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); + constructor(formState?: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); patchValue(value: any, options?: { onlySelf?: boolean; emitEvent?: boolean; @@ -283,7 +283,7 @@ export declare class FormGroup extends AbstractControl { }; constructor(controls: { [key: string]: AbstractControl; - }, validator?: ValidatorFn | null, asyncValidator?: AsyncValidatorFn | null); + }, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); addControl(name: string, control: AbstractControl): void; contains(controlName: string): boolean; getRawValue(): any; From 381471d33857c5c020ebd38df40c726fbb27c305 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Wed, 26 Jul 2017 14:53:59 -0700 Subject: [PATCH 02/53] fix(compiler): fix for element needing implicit parent placed in top-level ng-container fixes #18314 --- packages/compiler/src/ml_parser/parser.ts | 22 +- packages/compiler/src/ml_parser/tags.ts | 534 +++++++++--------- .../test/ml_parser/html_parser_spec.ts | 10 + 3 files changed, 285 insertions(+), 281 deletions(-) diff --git a/packages/compiler/src/ml_parser/parser.ts b/packages/compiler/src/ml_parser/parser.ts index e617156536..ab6a7c29f7 100644 --- a/packages/compiler/src/ml_parser/parser.ts +++ b/packages/compiler/src/ml_parser/parser.ts @@ -230,12 +230,9 @@ class _TreeBuilder { } private _closeVoidElement(): void { - if (this._elementStack.length > 0) { - const el = this._elementStack[this._elementStack.length - 1]; - - if (this.getTagDefinition(el.name).isVoid) { - this._elementStack.pop(); - } + const el = this._getParentElement(); + if (el && this.getTagDefinition(el.name).isVoid) { + this._elementStack.pop(); } } @@ -274,11 +271,10 @@ class _TreeBuilder { } private _pushElement(el: html.Element) { - if (this._elementStack.length > 0) { - const parentEl = this._elementStack[this._elementStack.length - 1]; - if (this.getTagDefinition(parentEl.name).isClosedByChild(el.name)) { - this._elementStack.pop(); - } + const parentEl = this._getParentElement(); + + if (parentEl && this.getTagDefinition(parentEl.name).isClosedByChild(el.name)) { + this._elementStack.pop(); } const tagDef = this.getTagDefinition(el.name); @@ -353,7 +349,7 @@ class _TreeBuilder { * `` elements are skipped as they are not rendered as DOM element. */ private _getParentElementSkippingContainers(): - {parent: html.Element, container: html.Element|null} { + {parent: html.Element | null, container: html.Element|null} { let container: html.Element|null = null; for (let i = this._elementStack.length - 1; i >= 0; i--) { @@ -363,7 +359,7 @@ class _TreeBuilder { container = this._elementStack[i]; } - return {parent: this._elementStack[this._elementStack.length - 1], container}; + return {parent: null, container}; } private _addToParent(node: html.Node) { diff --git a/packages/compiler/src/ml_parser/tags.ts b/packages/compiler/src/ml_parser/tags.ts index 0a882b6c51..5f06c3fd25 100644 --- a/packages/compiler/src/ml_parser/tags.ts +++ b/packages/compiler/src/ml_parser/tags.ts @@ -56,274 +56,272 @@ export function isNgTemplate(tagName: string): boolean { return splitNsName(tagName)[1] === 'ng-template'; } -export function getNsPrefix(fullName: string): string +export function getNsPrefix(fullName: string): string; export function getNsPrefix(fullName: null): null; - export function getNsPrefix(fullName: string | null): string | - null { - return fullName === null ? null : splitNsName(fullName)[0]; - } +export function getNsPrefix(fullName: string | null): string|null { + return fullName === null ? null : splitNsName(fullName)[0]; +} - export function mergeNsAndName(prefix: string, localName: string): - string { - return prefix ? `:${prefix}:${localName}` : localName; - } +export function mergeNsAndName(prefix: string, localName: string): string { + return prefix ? `:${prefix}:${localName}` : localName; +} - // see http://www.w3.org/TR/html51/syntax.html#named-character-references - // see https://html.spec.whatwg.org/multipage/entities.json - // This list is not exhaustive to keep the compiler footprint low. - // The `{` / `ƫ` syntax should be used when the named character reference does not - // exist. - export const NAMED_ENTITIES: {[k: string]: string} = { - 'Aacute': '\u00C1', - 'aacute': '\u00E1', - 'Acirc': '\u00C2', - 'acirc': '\u00E2', - 'acute': '\u00B4', - 'AElig': '\u00C6', - 'aelig': '\u00E6', - 'Agrave': '\u00C0', - 'agrave': '\u00E0', - 'alefsym': '\u2135', - 'Alpha': '\u0391', - 'alpha': '\u03B1', - 'amp': '&', - 'and': '\u2227', - 'ang': '\u2220', - 'apos': '\u0027', - 'Aring': '\u00C5', - 'aring': '\u00E5', - 'asymp': '\u2248', - 'Atilde': '\u00C3', - 'atilde': '\u00E3', - 'Auml': '\u00C4', - 'auml': '\u00E4', - 'bdquo': '\u201E', - 'Beta': '\u0392', - 'beta': '\u03B2', - 'brvbar': '\u00A6', - 'bull': '\u2022', - 'cap': '\u2229', - 'Ccedil': '\u00C7', - 'ccedil': '\u00E7', - 'cedil': '\u00B8', - 'cent': '\u00A2', - 'Chi': '\u03A7', - 'chi': '\u03C7', - 'circ': '\u02C6', - 'clubs': '\u2663', - 'cong': '\u2245', - 'copy': '\u00A9', - 'crarr': '\u21B5', - 'cup': '\u222A', - 'curren': '\u00A4', - 'dagger': '\u2020', - 'Dagger': '\u2021', - 'darr': '\u2193', - 'dArr': '\u21D3', - 'deg': '\u00B0', - 'Delta': '\u0394', - 'delta': '\u03B4', - 'diams': '\u2666', - 'divide': '\u00F7', - 'Eacute': '\u00C9', - 'eacute': '\u00E9', - 'Ecirc': '\u00CA', - 'ecirc': '\u00EA', - 'Egrave': '\u00C8', - 'egrave': '\u00E8', - 'empty': '\u2205', - 'emsp': '\u2003', - 'ensp': '\u2002', - 'Epsilon': '\u0395', - 'epsilon': '\u03B5', - 'equiv': '\u2261', - 'Eta': '\u0397', - 'eta': '\u03B7', - 'ETH': '\u00D0', - 'eth': '\u00F0', - 'Euml': '\u00CB', - 'euml': '\u00EB', - 'euro': '\u20AC', - 'exist': '\u2203', - 'fnof': '\u0192', - 'forall': '\u2200', - 'frac12': '\u00BD', - 'frac14': '\u00BC', - 'frac34': '\u00BE', - 'frasl': '\u2044', - 'Gamma': '\u0393', - 'gamma': '\u03B3', - 'ge': '\u2265', - 'gt': '>', - 'harr': '\u2194', - 'hArr': '\u21D4', - 'hearts': '\u2665', - 'hellip': '\u2026', - 'Iacute': '\u00CD', - 'iacute': '\u00ED', - 'Icirc': '\u00CE', - 'icirc': '\u00EE', - 'iexcl': '\u00A1', - 'Igrave': '\u00CC', - 'igrave': '\u00EC', - 'image': '\u2111', - 'infin': '\u221E', - 'int': '\u222B', - 'Iota': '\u0399', - 'iota': '\u03B9', - 'iquest': '\u00BF', - 'isin': '\u2208', - 'Iuml': '\u00CF', - 'iuml': '\u00EF', - 'Kappa': '\u039A', - 'kappa': '\u03BA', - 'Lambda': '\u039B', - 'lambda': '\u03BB', - 'lang': '\u27E8', - 'laquo': '\u00AB', - 'larr': '\u2190', - 'lArr': '\u21D0', - 'lceil': '\u2308', - 'ldquo': '\u201C', - 'le': '\u2264', - 'lfloor': '\u230A', - 'lowast': '\u2217', - 'loz': '\u25CA', - 'lrm': '\u200E', - 'lsaquo': '\u2039', - 'lsquo': '\u2018', - 'lt': '<', - 'macr': '\u00AF', - 'mdash': '\u2014', - 'micro': '\u00B5', - 'middot': '\u00B7', - 'minus': '\u2212', - 'Mu': '\u039C', - 'mu': '\u03BC', - 'nabla': '\u2207', - 'nbsp': '\u00A0', - 'ndash': '\u2013', - 'ne': '\u2260', - 'ni': '\u220B', - 'not': '\u00AC', - 'notin': '\u2209', - 'nsub': '\u2284', - 'Ntilde': '\u00D1', - 'ntilde': '\u00F1', - 'Nu': '\u039D', - 'nu': '\u03BD', - 'Oacute': '\u00D3', - 'oacute': '\u00F3', - 'Ocirc': '\u00D4', - 'ocirc': '\u00F4', - 'OElig': '\u0152', - 'oelig': '\u0153', - 'Ograve': '\u00D2', - 'ograve': '\u00F2', - 'oline': '\u203E', - 'Omega': '\u03A9', - 'omega': '\u03C9', - 'Omicron': '\u039F', - 'omicron': '\u03BF', - 'oplus': '\u2295', - 'or': '\u2228', - 'ordf': '\u00AA', - 'ordm': '\u00BA', - 'Oslash': '\u00D8', - 'oslash': '\u00F8', - 'Otilde': '\u00D5', - 'otilde': '\u00F5', - 'otimes': '\u2297', - 'Ouml': '\u00D6', - 'ouml': '\u00F6', - 'para': '\u00B6', - 'permil': '\u2030', - 'perp': '\u22A5', - 'Phi': '\u03A6', - 'phi': '\u03C6', - 'Pi': '\u03A0', - 'pi': '\u03C0', - 'piv': '\u03D6', - 'plusmn': '\u00B1', - 'pound': '\u00A3', - 'prime': '\u2032', - 'Prime': '\u2033', - 'prod': '\u220F', - 'prop': '\u221D', - 'Psi': '\u03A8', - 'psi': '\u03C8', - 'quot': '\u0022', - 'radic': '\u221A', - 'rang': '\u27E9', - 'raquo': '\u00BB', - 'rarr': '\u2192', - 'rArr': '\u21D2', - 'rceil': '\u2309', - 'rdquo': '\u201D', - 'real': '\u211C', - 'reg': '\u00AE', - 'rfloor': '\u230B', - 'Rho': '\u03A1', - 'rho': '\u03C1', - 'rlm': '\u200F', - 'rsaquo': '\u203A', - 'rsquo': '\u2019', - 'sbquo': '\u201A', - 'Scaron': '\u0160', - 'scaron': '\u0161', - 'sdot': '\u22C5', - 'sect': '\u00A7', - 'shy': '\u00AD', - 'Sigma': '\u03A3', - 'sigma': '\u03C3', - 'sigmaf': '\u03C2', - 'sim': '\u223C', - 'spades': '\u2660', - 'sub': '\u2282', - 'sube': '\u2286', - 'sum': '\u2211', - 'sup': '\u2283', - 'sup1': '\u00B9', - 'sup2': '\u00B2', - 'sup3': '\u00B3', - 'supe': '\u2287', - 'szlig': '\u00DF', - 'Tau': '\u03A4', - 'tau': '\u03C4', - 'there4': '\u2234', - 'Theta': '\u0398', - 'theta': '\u03B8', - 'thetasym': '\u03D1', - 'thinsp': '\u2009', - 'THORN': '\u00DE', - 'thorn': '\u00FE', - 'tilde': '\u02DC', - 'times': '\u00D7', - 'trade': '\u2122', - 'Uacute': '\u00DA', - 'uacute': '\u00FA', - 'uarr': '\u2191', - 'uArr': '\u21D1', - 'Ucirc': '\u00DB', - 'ucirc': '\u00FB', - 'Ugrave': '\u00D9', - 'ugrave': '\u00F9', - 'uml': '\u00A8', - 'upsih': '\u03D2', - 'Upsilon': '\u03A5', - 'upsilon': '\u03C5', - 'Uuml': '\u00DC', - 'uuml': '\u00FC', - 'weierp': '\u2118', - 'Xi': '\u039E', - 'xi': '\u03BE', - 'Yacute': '\u00DD', - 'yacute': '\u00FD', - 'yen': '\u00A5', - 'yuml': '\u00FF', - 'Yuml': '\u0178', - 'Zeta': '\u0396', - 'zeta': '\u03B6', - 'zwj': '\u200D', - 'zwnj': '\u200C', - }; +// see http://www.w3.org/TR/html51/syntax.html#named-character-references +// see https://html.spec.whatwg.org/multipage/entities.json +// This list is not exhaustive to keep the compiler footprint low. +// The `{` / `ƫ` syntax should be used when the named character reference does not +// exist. +export const NAMED_ENTITIES: {[k: string]: string} = { + 'Aacute': '\u00C1', + 'aacute': '\u00E1', + 'Acirc': '\u00C2', + 'acirc': '\u00E2', + 'acute': '\u00B4', + 'AElig': '\u00C6', + 'aelig': '\u00E6', + 'Agrave': '\u00C0', + 'agrave': '\u00E0', + 'alefsym': '\u2135', + 'Alpha': '\u0391', + 'alpha': '\u03B1', + 'amp': '&', + 'and': '\u2227', + 'ang': '\u2220', + 'apos': '\u0027', + 'Aring': '\u00C5', + 'aring': '\u00E5', + 'asymp': '\u2248', + 'Atilde': '\u00C3', + 'atilde': '\u00E3', + 'Auml': '\u00C4', + 'auml': '\u00E4', + 'bdquo': '\u201E', + 'Beta': '\u0392', + 'beta': '\u03B2', + 'brvbar': '\u00A6', + 'bull': '\u2022', + 'cap': '\u2229', + 'Ccedil': '\u00C7', + 'ccedil': '\u00E7', + 'cedil': '\u00B8', + 'cent': '\u00A2', + 'Chi': '\u03A7', + 'chi': '\u03C7', + 'circ': '\u02C6', + 'clubs': '\u2663', + 'cong': '\u2245', + 'copy': '\u00A9', + 'crarr': '\u21B5', + 'cup': '\u222A', + 'curren': '\u00A4', + 'dagger': '\u2020', + 'Dagger': '\u2021', + 'darr': '\u2193', + 'dArr': '\u21D3', + 'deg': '\u00B0', + 'Delta': '\u0394', + 'delta': '\u03B4', + 'diams': '\u2666', + 'divide': '\u00F7', + 'Eacute': '\u00C9', + 'eacute': '\u00E9', + 'Ecirc': '\u00CA', + 'ecirc': '\u00EA', + 'Egrave': '\u00C8', + 'egrave': '\u00E8', + 'empty': '\u2205', + 'emsp': '\u2003', + 'ensp': '\u2002', + 'Epsilon': '\u0395', + 'epsilon': '\u03B5', + 'equiv': '\u2261', + 'Eta': '\u0397', + 'eta': '\u03B7', + 'ETH': '\u00D0', + 'eth': '\u00F0', + 'Euml': '\u00CB', + 'euml': '\u00EB', + 'euro': '\u20AC', + 'exist': '\u2203', + 'fnof': '\u0192', + 'forall': '\u2200', + 'frac12': '\u00BD', + 'frac14': '\u00BC', + 'frac34': '\u00BE', + 'frasl': '\u2044', + 'Gamma': '\u0393', + 'gamma': '\u03B3', + 'ge': '\u2265', + 'gt': '>', + 'harr': '\u2194', + 'hArr': '\u21D4', + 'hearts': '\u2665', + 'hellip': '\u2026', + 'Iacute': '\u00CD', + 'iacute': '\u00ED', + 'Icirc': '\u00CE', + 'icirc': '\u00EE', + 'iexcl': '\u00A1', + 'Igrave': '\u00CC', + 'igrave': '\u00EC', + 'image': '\u2111', + 'infin': '\u221E', + 'int': '\u222B', + 'Iota': '\u0399', + 'iota': '\u03B9', + 'iquest': '\u00BF', + 'isin': '\u2208', + 'Iuml': '\u00CF', + 'iuml': '\u00EF', + 'Kappa': '\u039A', + 'kappa': '\u03BA', + 'Lambda': '\u039B', + 'lambda': '\u03BB', + 'lang': '\u27E8', + 'laquo': '\u00AB', + 'larr': '\u2190', + 'lArr': '\u21D0', + 'lceil': '\u2308', + 'ldquo': '\u201C', + 'le': '\u2264', + 'lfloor': '\u230A', + 'lowast': '\u2217', + 'loz': '\u25CA', + 'lrm': '\u200E', + 'lsaquo': '\u2039', + 'lsquo': '\u2018', + 'lt': '<', + 'macr': '\u00AF', + 'mdash': '\u2014', + 'micro': '\u00B5', + 'middot': '\u00B7', + 'minus': '\u2212', + 'Mu': '\u039C', + 'mu': '\u03BC', + 'nabla': '\u2207', + 'nbsp': '\u00A0', + 'ndash': '\u2013', + 'ne': '\u2260', + 'ni': '\u220B', + 'not': '\u00AC', + 'notin': '\u2209', + 'nsub': '\u2284', + 'Ntilde': '\u00D1', + 'ntilde': '\u00F1', + 'Nu': '\u039D', + 'nu': '\u03BD', + 'Oacute': '\u00D3', + 'oacute': '\u00F3', + 'Ocirc': '\u00D4', + 'ocirc': '\u00F4', + 'OElig': '\u0152', + 'oelig': '\u0153', + 'Ograve': '\u00D2', + 'ograve': '\u00F2', + 'oline': '\u203E', + 'Omega': '\u03A9', + 'omega': '\u03C9', + 'Omicron': '\u039F', + 'omicron': '\u03BF', + 'oplus': '\u2295', + 'or': '\u2228', + 'ordf': '\u00AA', + 'ordm': '\u00BA', + 'Oslash': '\u00D8', + 'oslash': '\u00F8', + 'Otilde': '\u00D5', + 'otilde': '\u00F5', + 'otimes': '\u2297', + 'Ouml': '\u00D6', + 'ouml': '\u00F6', + 'para': '\u00B6', + 'permil': '\u2030', + 'perp': '\u22A5', + 'Phi': '\u03A6', + 'phi': '\u03C6', + 'Pi': '\u03A0', + 'pi': '\u03C0', + 'piv': '\u03D6', + 'plusmn': '\u00B1', + 'pound': '\u00A3', + 'prime': '\u2032', + 'Prime': '\u2033', + 'prod': '\u220F', + 'prop': '\u221D', + 'Psi': '\u03A8', + 'psi': '\u03C8', + 'quot': '\u0022', + 'radic': '\u221A', + 'rang': '\u27E9', + 'raquo': '\u00BB', + 'rarr': '\u2192', + 'rArr': '\u21D2', + 'rceil': '\u2309', + 'rdquo': '\u201D', + 'real': '\u211C', + 'reg': '\u00AE', + 'rfloor': '\u230B', + 'Rho': '\u03A1', + 'rho': '\u03C1', + 'rlm': '\u200F', + 'rsaquo': '\u203A', + 'rsquo': '\u2019', + 'sbquo': '\u201A', + 'Scaron': '\u0160', + 'scaron': '\u0161', + 'sdot': '\u22C5', + 'sect': '\u00A7', + 'shy': '\u00AD', + 'Sigma': '\u03A3', + 'sigma': '\u03C3', + 'sigmaf': '\u03C2', + 'sim': '\u223C', + 'spades': '\u2660', + 'sub': '\u2282', + 'sube': '\u2286', + 'sum': '\u2211', + 'sup': '\u2283', + 'sup1': '\u00B9', + 'sup2': '\u00B2', + 'sup3': '\u00B3', + 'supe': '\u2287', + 'szlig': '\u00DF', + 'Tau': '\u03A4', + 'tau': '\u03C4', + 'there4': '\u2234', + 'Theta': '\u0398', + 'theta': '\u03B8', + 'thetasym': '\u03D1', + 'thinsp': '\u2009', + 'THORN': '\u00DE', + 'thorn': '\u00FE', + 'tilde': '\u02DC', + 'times': '\u00D7', + 'trade': '\u2122', + 'Uacute': '\u00DA', + 'uacute': '\u00FA', + 'uarr': '\u2191', + 'uArr': '\u21D1', + 'Ucirc': '\u00DB', + 'ucirc': '\u00FB', + 'Ugrave': '\u00D9', + 'ugrave': '\u00F9', + 'uml': '\u00A8', + 'upsih': '\u03D2', + 'Upsilon': '\u03A5', + 'upsilon': '\u03C5', + 'Uuml': '\u00DC', + 'uuml': '\u00FC', + 'weierp': '\u2118', + 'Xi': '\u039E', + 'xi': '\u03BE', + 'Yacute': '\u00DD', + 'yacute': '\u00FD', + 'yen': '\u00A5', + 'yuml': '\u00FF', + 'Yuml': '\u0178', + 'Zeta': '\u0396', + 'zeta': '\u03B6', + 'zwj': '\u200D', + 'zwnj': '\u200C', +}; diff --git a/packages/compiler/test/ml_parser/html_parser_spec.ts b/packages/compiler/test/ml_parser/html_parser_spec.ts index 1a8b2bd630..837ec0ebb0 100644 --- a/packages/compiler/test/ml_parser/html_parser_spec.ts +++ b/packages/compiler/test/ml_parser/html_parser_spec.ts @@ -152,6 +152,16 @@ export function main() { ]); }); + it('should append the required parent considering top level ng-container', () => { + expect(humanizeDom( + parser.parse('

', 'TestComp'))) + .toEqual([ + [html.Element, 'ng-container', 0], + [html.Element, 'tr', 1], + [html.Element, 'p', 0], + ]); + }); + it('should special case ng-container when adding a required parent', () => { expect(humanizeDom(parser.parse( '
', From 67dff7bd5d552d65b0e323c9da6a14b911ba958a Mon Sep 17 00:00:00 2001 From: Chuck Jazdzewski Date: Wed, 26 Jul 2017 14:58:07 -0700 Subject: [PATCH 03/53] feat(tsc-wrapped): allow values to be substituted by collector clients Also reenabled tests that were unintentionally disabled when they were moved from tools/@angular. --- packages/tsc-wrapped/src/collector.ts | 24 +++++++++-- packages/tsc-wrapped/src/evaluator.ts | 19 ++++++--- .../{collector.spec.ts => collector_spec.ts} | 40 ++++++++++++++++++- .../{evaluator.spec.ts => evaluator_spec.ts} | 39 ++++++++++++++++++ .../test/{main.spec.ts => main_spec.ts} | 0 .../test/{symbols.spec.ts => symbols_spec.ts} | 0 .../test/{tsc.spec.ts => tsc_spec.ts} | 0 7 files changed, 113 insertions(+), 9 deletions(-) rename packages/tsc-wrapped/test/{collector.spec.ts => collector_spec.ts} (97%) rename packages/tsc-wrapped/test/{evaluator.spec.ts => evaluator_spec.ts} (91%) rename packages/tsc-wrapped/test/{main.spec.ts => main_spec.ts} (100%) rename packages/tsc-wrapped/test/{symbols.spec.ts => symbols_spec.ts} (100%) rename packages/tsc-wrapped/test/{tsc.spec.ts => tsc_spec.ts} (100%) diff --git a/packages/tsc-wrapped/src/collector.ts b/packages/tsc-wrapped/src/collector.ts index f6d117d200..3ad18800fc 100644 --- a/packages/tsc-wrapped/src/collector.ts +++ b/packages/tsc-wrapped/src/collector.ts @@ -26,7 +26,7 @@ const isStatic = (ts as any).ModifierFlags ? /** * A set of collector options to use when collecting metadata. */ -export class CollectorOptions { +export interface CollectorOptions { /** * Version of the metadata to collect. */ @@ -42,6 +42,11 @@ export class CollectorOptions { * Do not simplify invalid expressions. */ verboseInvalidExpression?: boolean; + + /** + * An expression substitution callback. + */ + substituteExpression?: (value: MetadataValue, node: ts.Node) => MetadataValue; } /** @@ -54,12 +59,25 @@ export class MetadataCollector { * Returns a JSON.stringify friendly form describing the decorators of the exported classes from * the source file that is expected to correspond to a module. */ - public getMetadata(sourceFile: ts.SourceFile, strict: boolean = false): ModuleMetadata|undefined { + public getMetadata( + sourceFile: ts.SourceFile, strict: boolean = false, + substituteExpression?: (value: MetadataValue, node: ts.Node) => MetadataValue): ModuleMetadata + |undefined { const locals = new Symbols(sourceFile); const nodeMap = new Map(); - const evaluator = new Evaluator(locals, nodeMap, this.options); + const composedSubstituter = substituteExpression && this.options.substituteExpression ? + (value: MetadataValue, node: ts.Node) => + this.options.substituteExpression !(substituteExpression(value, node), node) : + substituteExpression; + const evaluatorOptions = substituteExpression ? + {...this.options, substituteExpression: composedSubstituter} : + this.options; let metadata: {[name: string]: MetadataValue | ClassMetadata | FunctionMetadata}|undefined; + const evaluator = new Evaluator(locals, nodeMap, evaluatorOptions, (name, value) => { + if (!metadata) metadata = {}; + metadata[name] = value; + }); let exports: ModuleExportMetadata[]|undefined = undefined; function objFromDecorator(decoratorNode: ts.Decorator): MetadataSymbolicExpression { diff --git a/packages/tsc-wrapped/src/evaluator.ts b/packages/tsc-wrapped/src/evaluator.ts index b77f55c027..27e5afc9af 100644 --- a/packages/tsc-wrapped/src/evaluator.ts +++ b/packages/tsc-wrapped/src/evaluator.ts @@ -9,9 +9,10 @@ import * as ts from 'typescript'; import {CollectorOptions} from './collector'; -import {MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataSymbolicCallExpression, MetadataValue, isMetadataError, isMetadataModuleReferenceExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSpreadExpression} from './schema'; +import {MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataSymbolicCallExpression, MetadataValue, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSpreadExpression} from './schema'; import {Symbols} from './symbols'; + // In TypeScript 2.1 the spread element kind was renamed. const spreadElementSyntaxKind: ts.SyntaxKind = (ts.SyntaxKind as any).SpreadElement || (ts.SyntaxKind as any).SpreadElementExpression; @@ -104,7 +105,8 @@ export function errorSymbol( export class Evaluator { constructor( private symbols: Symbols, private nodeMap: Map, - private options: CollectorOptions = {}) {} + private options: CollectorOptions = {}, + private recordExport?: (name: string, value: MetadataValue) => void) {} nameOf(node: ts.Node|undefined): string|MetadataError { if (node && node.kind == ts.SyntaxKind.Identifier) { @@ -232,7 +234,14 @@ export class Evaluator { const t = this; let error: MetadataError|undefined; - function recordEntry(entry: T, node: ts.Node): T { + function recordEntry(entry: MetadataValue, node: ts.Node): MetadataValue { + if (t.options.substituteExpression) { + const newEntry = t.options.substituteExpression(entry, node); + if (t.recordExport && newEntry != entry && isMetadataGlobalReferenceExpression(newEntry)) { + t.recordExport(newEntry.name, entry); + } + entry = newEntry; + } t.nodeMap.set(entry, node); return entry; } @@ -283,7 +292,7 @@ export class Evaluator { if (this.options.quotedNames && quoted.length) { obj['$quoted$'] = quoted; } - return obj; + return recordEntry(obj, node); case ts.SyntaxKind.ArrayLiteralExpression: let arr: MetadataValue[] = []; ts.forEachChild(node, child => { @@ -308,7 +317,7 @@ export class Evaluator { arr.push(value); }); if (error) return error; - return arr; + return recordEntry(arr, node); case spreadElementSyntaxKind: let spreadExpression = this.evaluateNode((node as any).expression); return recordEntry({__symbolic: 'spread', expression: spreadExpression}, node); diff --git a/packages/tsc-wrapped/test/collector.spec.ts b/packages/tsc-wrapped/test/collector_spec.ts similarity index 97% rename from packages/tsc-wrapped/test/collector.spec.ts rename to packages/tsc-wrapped/test/collector_spec.ts index b22f3d1f5b..7f95df423c 100644 --- a/packages/tsc-wrapped/test/collector.spec.ts +++ b/packages/tsc-wrapped/test/collector_spec.ts @@ -9,7 +9,7 @@ import * as ts from 'typescript'; import {MetadataCollector} from '../src/collector'; -import {ClassMetadata, ConstructorMetadata, MetadataEntry, ModuleMetadata, isClassMetadata} from '../src/schema'; +import {ClassMetadata, ConstructorMetadata, MetadataEntry, ModuleMetadata, isClassMetadata, isMetadataGlobalReferenceExpression} from '../src/schema'; import {Directory, Host, expectValidSources} from './typescript.mocks'; @@ -939,6 +939,44 @@ describe('Collector', () => { }); }); + describe('substitutions', () => { + const lambdaTemp = 'lambdaTemp'; + + it('should be able to substitute a lambda', () => { + const source = createSource(` + const b = 1; + export const a = () => b; + `); + const metadata = collector.getMetadata(source, /* strict */ false, (value, node) => { + if (node.kind === ts.SyntaxKind.ArrowFunction) { + return {__symbolic: 'reference', name: lambdaTemp}; + } + return value; + }); + expect(metadata !.metadata['a']).toEqual({__symbolic: 'reference', name: lambdaTemp}); + }); + + it('should compose substitution functions', () => { + const collector = new MetadataCollector({ + substituteExpression: (value, node) => isMetadataGlobalReferenceExpression(value) && + value.name == lambdaTemp ? + {__symbolic: 'reference', name: value.name + '2'} : + value + }); + const source = createSource(` + const b = 1; + export const a = () => b; + `); + const metadata = collector.getMetadata(source, /* strict */ false, (value, node) => { + if (node.kind === ts.SyntaxKind.ArrowFunction) { + return {__symbolic: 'reference', name: lambdaTemp}; + } + return value; + }); + expect(metadata !.metadata['a']).toEqual({__symbolic: 'reference', name: lambdaTemp + '2'}); + }); + }); + function override(fileName: string, content: string) { host.overrideFile(fileName, content); host.addFile(fileName); diff --git a/packages/tsc-wrapped/test/evaluator.spec.ts b/packages/tsc-wrapped/test/evaluator_spec.ts similarity index 91% rename from packages/tsc-wrapped/test/evaluator.spec.ts rename to packages/tsc-wrapped/test/evaluator_spec.ts index 2205a27120..bc6a75d471 100644 --- a/packages/tsc-wrapped/test/evaluator.spec.ts +++ b/packages/tsc-wrapped/test/evaluator_spec.ts @@ -223,6 +223,45 @@ describe('Evaluator', () => { expect(evaluator.evaluateNode(expr.initializer !)) .toEqual({__symbolic: 'new', expression: {__symbolic: 'reference', name: 'f'}}); }); + + describe('with substitution', () => { + let evaluator: Evaluator; + const lambdaTemp = 'lambdaTemp'; + + beforeEach(() => { + evaluator = new Evaluator(symbols, new Map(), { + substituteExpression: (value, node) => { + if (node.kind == ts.SyntaxKind.ArrowFunction) { + return {__symbolic: 'reference', name: lambdaTemp}; + } + return value; + } + }); + }); + + it('should be able to substitute a lambda with a reference', () => { + const source = sourceFileOf(` + var b = 1; + export var a = () => b; + `); + const expr = findVar(source, 'a'); + expect(evaluator.evaluateNode(expr !.initializer !)) + .toEqual({__symbolic: 'reference', name: lambdaTemp}); + }); + + it('should be able to substitute a lambda in an expression', () => { + const source = sourceFileOf(` + var b = 1; + export var a = [ + { provide: 'someValue': useFactory: () => b } + ]; + `); + const expr = findVar(source, 'a'); + expect(evaluator.evaluateNode(expr !.initializer !)).toEqual([ + {provide: 'someValue', useFactory: {__symbolic: 'reference', name: lambdaTemp}} + ]) + }); + }) }); function sourceFileOf(text: string): ts.SourceFile { diff --git a/packages/tsc-wrapped/test/main.spec.ts b/packages/tsc-wrapped/test/main_spec.ts similarity index 100% rename from packages/tsc-wrapped/test/main.spec.ts rename to packages/tsc-wrapped/test/main_spec.ts diff --git a/packages/tsc-wrapped/test/symbols.spec.ts b/packages/tsc-wrapped/test/symbols_spec.ts similarity index 100% rename from packages/tsc-wrapped/test/symbols.spec.ts rename to packages/tsc-wrapped/test/symbols_spec.ts diff --git a/packages/tsc-wrapped/test/tsc.spec.ts b/packages/tsc-wrapped/test/tsc_spec.ts similarity index 100% rename from packages/tsc-wrapped/test/tsc.spec.ts rename to packages/tsc-wrapped/test/tsc_spec.ts From b6c4af649505a7d158c7d4c0df4d43270bd29fd7 Mon Sep 17 00:00:00 2001 From: Chuck Jazdzewski Date: Thu, 13 Jul 2017 14:25:17 -0700 Subject: [PATCH 04/53] feat(compiler-cli): automatically lower lambda expressions in metadata --- packages/compiler-cli/src/compiler_host.ts | 10 +- .../src/path_mapped_compiler_host.ts | 2 +- packages/compiler-cli/src/transformers/api.ts | 4 + .../src/transformers/entry_points.ts | 8 + .../src/transformers/lower_expressions.ts | 177 ++++++++++++++++++ .../transformers/module_filename_resolver.ts | 8 + .../compiler-cli/src/transformers/program.ts | 42 +++-- packages/compiler-cli/test/ngc_spec.ts | 118 +++++++++++- .../transformers/lower_expressions_spec.ts | 107 +++++++++++ 9 files changed, 456 insertions(+), 20 deletions(-) create mode 100644 packages/compiler-cli/src/transformers/lower_expressions.ts create mode 100644 packages/compiler-cli/test/transformers/lower_expressions_spec.ts diff --git a/packages/compiler-cli/src/compiler_host.ts b/packages/compiler-cli/src/compiler_host.ts index 04d5083770..e7c5c03954 100644 --- a/packages/compiler-cli/src/compiler_host.ts +++ b/packages/compiler-cli/src/compiler_host.ts @@ -25,8 +25,9 @@ export interface CompilerHostContext extends ts.ModuleResolutionHost { assumeFileExists(fileName: string): void; } +export interface MetadataProvider { getMetadata(source: ts.SourceFile): ModuleMetadata|undefined; } + export class CompilerHost implements AotCompilerHost { - protected metadataCollector = new MetadataCollector(); private isGenDirChildOfRootDir: boolean; protected basePath: string; private genDir: string; @@ -39,7 +40,8 @@ export class CompilerHost implements AotCompilerHost { constructor( protected program: ts.Program, protected options: AngularCompilerOptions, - protected context: CompilerHostContext, collectorOptions?: CollectorOptions) { + protected context: CompilerHostContext, collectorOptions?: CollectorOptions, + protected metadataProvider: MetadataProvider = new MetadataCollector()) { // normalize the path so that it never ends with '/'. this.basePath = path.normalize(path.join(this.options.basePath !, '.')).replace(/\\/g, '/'); this.genDir = path.normalize(path.join(this.options.genDir !, '.')).replace(/\\/g, '/'); @@ -206,7 +208,7 @@ export class CompilerHost implements AotCompilerHost { } const sf = this.getSourceFile(filePath); - const metadata = this.metadataCollector.getMetadata(sf); + const metadata = this.metadataProvider.getMetadata(sf); return metadata ? [metadata] : []; } @@ -245,7 +247,7 @@ export class CompilerHost implements AotCompilerHost { v3Metadata.metadata[prop] = v1Metadata.metadata[prop]; } - const exports = this.metadataCollector.getMetadata(this.getSourceFile(dtsFilePath)); + const exports = this.metadataProvider.getMetadata(this.getSourceFile(dtsFilePath)); if (exports) { for (let prop in exports.metadata) { if (!v3Metadata.metadata[prop]) { diff --git a/packages/compiler-cli/src/path_mapped_compiler_host.ts b/packages/compiler-cli/src/path_mapped_compiler_host.ts index 2463b5b4ee..f17b0a5800 100644 --- a/packages/compiler-cli/src/path_mapped_compiler_host.ts +++ b/packages/compiler-cli/src/path_mapped_compiler_host.ts @@ -132,7 +132,7 @@ export class PathMappedCompilerHost extends CompilerHost { } else { const sf = this.getSourceFile(rootedPath); sf.fileName = sf.fileName; - const metadata = this.metadataCollector.getMetadata(sf); + const metadata = this.metadataProvider.getMetadata(sf); return metadata ? [metadata] : []; } } diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index ea0919c3b5..de1ccc3b9d 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -91,6 +91,10 @@ export interface CompilerOptions extends ts.CompilerOptions { // Whether to enable support for