diff --git a/.travis.yml b/.travis.yml index d574adc770..04c0c2a707 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,7 +53,7 @@ env: - CI_MODE=browserstack_required - CI_MODE=saucelabs_optional - CI_MODE=browserstack_optional - - CI_MODE=docs_test + - CI_MODE=aio_tools_test - CI_MODE=aio - CI_MODE=aio_e2e AIO_SHARD=0 - CI_MODE=aio_e2e AIO_SHARD=1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c0e3ab0a3..b46f8db760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ + +# [5.0.0-beta.2](https://github.com/angular/angular/compare/5.0.0-beta.1...5.0.0-beta.2) (2017-08-02) + + +### Bug Fixes + +* **compiler:** do not consider arguments when determining recursion ([e64b54b](https://github.com/angular/angular/commit/e64b54b)) +* **compiler:** fix for element needing implicit parent placed in top-level ng-container ([381471d](https://github.com/angular/angular/commit/381471d)), closes [#18314](https://github.com/angular/angular/issues/18314) + + +### Features + +* **forms:** add options arg to abstract controls ([ebef5e6](https://github.com/angular/angular/commit/ebef5e6)) +* **router:** add events tracking activation of individual routes ([49cd851](https://github.com/angular/angular/commit/49cd851)) + + + + +## [4.3.3](https://github.com/angular/angular/compare/4.3.2...4.3.3) (2017-08-02) + + +### Bug Fixes + +* **compiler:** fix for element needing implicit parent placed in top-level ng-container ([f5cbc2e](https://github.com/angular/angular/commit/f5cbc2e)), closes [#18314](https://github.com/angular/angular/issues/18314) + + + # [5.0.0-beta.1](https://github.com/angular/angular/compare/5.0.0-beta.0...5.0.0-beta.1) (2017-07-27) diff --git a/aio/.angular-cli.json b/aio/.angular-cli.json index 6b3ef2df2f..10b25f2812 100644 --- a/aio/.angular-cli.json +++ b/aio/.angular-cli.json @@ -31,8 +31,9 @@ "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", - "stage": "environments/environment.stage.ts", - "prod": "environments/environment.prod.ts" + "next": "environments/environment.next.ts", + "stable": "environments/environment.stable.ts", + "archive": "environments/environment.archive.ts" } } ], diff --git a/aio/content/examples/form-validation/e2e-spec.ts b/aio/content/examples/form-validation/e2e-spec.ts index 1e9788956a..cd17cd4e3d 100644 --- a/aio/content/examples/form-validation/e2e-spec.ts +++ b/aio/content/examples/form-validation/e2e-spec.ts @@ -9,30 +9,20 @@ describe('Form Validation Tests', function () { browser.get(''); }); - describe('Hero Form 1', () => { + describe('Template-driven form', () => { beforeAll(() => { - getPage('hero-form-template1'); + getPage('hero-form-template'); }); - tests(); + tests('Template-Driven Form'); }); - describe('Hero Form 2', () => { + describe('Reactive form', () => { beforeAll(() => { - getPage('hero-form-template2'); + getPage('hero-form-reactive'); }); - tests(); - bobTests(); - }); - - describe('Hero Form 3 (Reactive)', () => { - beforeAll(() => { - getPage('hero-form-reactive3'); - makeNameTooLong(); - }); - - tests(); + tests('Reactive Form'); bobTests(); }); }); @@ -48,6 +38,7 @@ let page: { nameInput: ElementFinder, alterEgoInput: ElementFinder, powerSelect: ElementFinder, + powerOption: ElementFinder, errorMessages: ElementArrayFinder, heroFormButtons: ElementArrayFinder, heroSubmitted: ElementFinder @@ -64,19 +55,21 @@ function getPage(sectionTag: string) { nameInput: section.element(by.css('#name')), alterEgoInput: section.element(by.css('#alterEgo')), powerSelect: section.element(by.css('#power')), + powerOption: section.element(by.css('#power option')), errorMessages: section.all(by.css('div.alert')), heroFormButtons: buttons, - heroSubmitted: section.element(by.css('hero-submitted > div')) + heroSubmitted: section.element(by.css('.submitted-message')) }; } -function tests() { +function tests(title: string) { + it('should display correct title', function () { - expect(page.title.getText()).toContain('Hero Form'); + expect(page.title.getText()).toContain(title); }); it('should not display submitted message before submit', function () { - expect(page.heroSubmitted.isElementPresent(by.css('h2'))).toBe(false); + expect(page.heroSubmitted.isElementPresent(by.css('p'))).toBe(false); }); it('should have form buttons', function () { @@ -130,11 +123,11 @@ function tests() { it('should hide form after submit', function () { page.heroFormButtons.get(0).click(); - expect(page.title.isDisplayed()).toBe(false); + expect(page.heroFormButtons.get(0).isDisplayed()).toBe(false); }); it('submitted form should be displayed', function () { - expect(page.heroSubmitted.isElementPresent(by.css('h2'))).toBe(true); + expect(page.heroSubmitted.isElementPresent(by.css('p'))).toBe(true); }); it('submitted form should have new hero name', function () { @@ -142,9 +135,9 @@ function tests() { }); it('clicking edit button should reveal form again', function () { - const editBtn = page.heroSubmitted.element(by.css('button')); - editBtn.click(); - expect(page.heroSubmitted.isElementPresent(by.css('h2'))) + const newFormBtn = page.heroSubmitted.element(by.css('button')); + newFormBtn.click(); + expect(page.heroSubmitted.isElementPresent(by.css('p'))) .toBe(false, 'submitted hidden again'); expect(page.title.isDisplayed()).toBe(true, 'can see form title'); }); @@ -159,9 +152,13 @@ function expectFormIsInvalid() { } function bobTests() { - const emsg = 'Someone named "Bob" cannot be a hero.'; + const emsg = 'Name cannot be Bob.'; it('should produce "no bob" error after setting name to "Bobby"', function () { + // Re-populate select element + page.powerSelect.click(); + page.powerOption.click(); + page.nameInput.clear(); page.nameInput.sendKeys('Bobby'); expectFormIsInvalid(); @@ -174,8 +171,3 @@ function bobTests() { expectFormIsValid(); }); } - -function makeNameTooLong() { - // make the first name invalid - page.nameInput.sendKeys('ThisHeroNameHasWayWayTooManyLetters'); -} diff --git a/aio/content/examples/form-validation/src/app/app.component.ts b/aio/content/examples/form-validation/src/app/app.component.ts index 2da4dc4d0a..5260b8d9e1 100644 --- a/aio/content/examples/form-validation/src/app/app.component.ts +++ b/aio/content/examples/form-validation/src/app/app.component.ts @@ -3,10 +3,8 @@ import { Component } from '@angular/core'; @Component({ selector: 'my-app', - template: ` + template: `
- -
- ` + ` }) export class AppComponent { } diff --git a/aio/content/examples/form-validation/src/app/app.module.ts b/aio/content/examples/form-validation/src/app/app.module.ts index 72b4e3a770..0a5d1cf455 100644 --- a/aio/content/examples/form-validation/src/app/app.module.ts +++ b/aio/content/examples/form-validation/src/app/app.module.ts @@ -1,18 +1,26 @@ // #docregion import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; -import { HeroFormTemplateModule } from './template/hero-form-template.module'; -import { HeroFormReactiveModule } from './reactive/hero-form-reactive.module'; +import { HeroFormTemplateComponent } from './template/hero-form-template.component'; +import { HeroFormReactiveComponent } from './reactive/hero-form-reactive.component'; +import { ForbiddenValidatorDirective } from './shared/forbidden-name.directive'; + @NgModule({ imports: [ BrowserModule, - HeroFormTemplateModule, - HeroFormReactiveModule + FormsModule, + ReactiveFormsModule + ], + declarations: [ + AppComponent, + HeroFormTemplateComponent, + HeroFormReactiveComponent, + ForbiddenValidatorDirective ], - declarations: [ AppComponent ], bootstrap: [ AppComponent ] }) export class AppModule { } diff --git a/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.html b/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.html index 149537bd3e..36cebbaca4 100644 --- a/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.html +++ b/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.html @@ -1,26 +1,38 @@
-
-

Hero Form 3 (Reactive)

- -
- -
- - - Reactive Form + + + +
+ +
+ + + + -
- {{ formErrors.name }} +
+ +
+ Name is required. +
+
+ Name must be at least 4 characters long. +
+
+ Name cannot be Bob. +
-
@@ -31,17 +43,20 @@ -
- {{ formErrors.power }} +
+
Power is required.
+ [disabled]="heroForm.invalid">Submit - -
+ (click)="formDir.resetForm({})">Reset +
+ - +
diff --git a/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.ts b/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.ts index 241ff1e782..7ce9e57fa2 100644 --- a/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.ts +++ b/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.ts @@ -2,115 +2,39 @@ // #docplaster // #docregion import { Component, OnInit } from '@angular/core'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; - -import { Hero } from '../shared/hero'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; import { forbiddenNameValidator } from '../shared/forbidden-name.directive'; @Component({ - selector: 'hero-form-reactive3', + selector: 'hero-form-reactive', templateUrl: './hero-form-reactive.component.html' }) export class HeroFormReactiveComponent implements OnInit { powers = ['Really Smart', 'Super Flexible', 'Weather Changer']; - hero = new Hero(18, 'Dr. WhatIsHisName', this.powers[0], 'Dr. What'); + hero = {name: 'Dr.', alterEgo: 'Dr. What', power: this.powers[0]}; - submitted = false; - - // #docregion on-submit - onSubmit() { - this.submitted = true; - this.hero = this.heroForm.value; - } - // #enddocregion on-submit -// #enddocregion - - // Reset the form with a new hero AND restore 'pristine' class state - // by toggling 'active' flag which causes the form - // to be removed/re-added in a tick via NgIf - // TODO: Workaround until NgForm has a reset method (#6822) - active = true; -// #docregion class - // #docregion add-hero - addHero() { - this.hero = new Hero(42, '', ''); - this.buildForm(); - // #enddocregion add-hero -// #enddocregion class - - this.active = false; - setTimeout(() => this.active = true, 0); -// #docregion - // #docregion add-hero - } - // #enddocregion add-hero - - // #docregion form-builder heroForm: FormGroup; - constructor(private fb: FormBuilder) { } + // #docregion form-group ngOnInit(): void { - this.buildForm(); - } - - buildForm(): void { - this.heroForm = this.fb.group({ - // #docregion name-validators - 'name': [this.hero.name, [ - Validators.required, - Validators.minLength(4), - Validators.maxLength(24), - forbiddenNameValidator(/bob/i) - ] - ], - // #enddocregion name-validators - 'alterEgo': [this.hero.alterEgo], - 'power': [this.hero.power, Validators.required] + // #docregion custom-validator + this.heroForm = new FormGroup({ + 'name': new FormControl(this.hero.name, [ + Validators.required, + Validators.minLength(4), + forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator. + ]), + 'alterEgo': new FormControl(this.hero.alterEgo), + 'power': new FormControl(this.hero.power, Validators.required) }); - - this.heroForm.valueChanges - .subscribe(data => this.onValueChanged(data)); - - this.onValueChanged(); // (re)set validation messages now + // #enddocregion custom-validator } - // #enddocregion form-builder + get name() { return this.heroForm.get('name'); } - onValueChanged(data?: any) { - if (!this.heroForm) { return; } - const form = this.heroForm; - - for (const field in this.formErrors) { - // clear previous error message (if any) - this.formErrors[field] = ''; - const control = form.get(field); - - if (control && control.dirty && !control.valid) { - const messages = this.validationMessages[field]; - for (const key in control.errors) { - this.formErrors[field] += messages[key] + ' '; - } - } - } - } - - formErrors = { - 'name': '', - 'power': '' - }; - - validationMessages = { - 'name': { - 'required': 'Name is required.', - 'minlength': 'Name must be at least 4 characters long.', - 'maxlength': 'Name cannot be more than 24 characters long.', - 'forbiddenName': 'Someone named "Bob" cannot be a hero.' - }, - 'power': { - 'required': 'Power is required.' - } - }; + get power() { return this.heroForm.get('power'); } + // #enddocregion form-group } // #enddocregion diff --git a/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.module.ts b/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.module.ts deleted file mode 100644 index 6ff9265e92..0000000000 --- a/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -// #docregion -import { NgModule } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; - -import { SharedModule } from '../shared/shared.module'; -import { HeroFormReactiveComponent } from './hero-form-reactive.component'; - -@NgModule({ - imports: [ SharedModule, ReactiveFormsModule ], - declarations: [ HeroFormReactiveComponent ], - exports: [ HeroFormReactiveComponent ] -}) -export class HeroFormReactiveModule { } diff --git a/aio/content/examples/form-validation/src/app/shared/forbidden-name.directive.ts b/aio/content/examples/form-validation/src/app/shared/forbidden-name.directive.ts index 870f514842..f78b8fb25b 100644 --- a/aio/content/examples/form-validation/src/app/shared/forbidden-name.directive.ts +++ b/aio/content/examples/form-validation/src/app/shared/forbidden-name.directive.ts @@ -6,9 +6,8 @@ import { AbstractControl, NG_VALIDATORS, Validator, ValidatorFn, Validators } fr /** A hero's name can't match the given regular expression */ export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn { return (control: AbstractControl): {[key: string]: any} => { - const name = control.value; - const no = nameRe.test(name); - return no ? {'forbiddenName': {name}} : null; + const forbidden = nameRe.test(control.value); + return forbidden ? {'forbiddenName': {value: control.value}} : null; }; } // #enddocregion custom-validator @@ -20,23 +19,12 @@ export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn { providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}] // #enddocregion directive-providers }) -export class ForbiddenValidatorDirective implements Validator, OnChanges { +export class ForbiddenValidatorDirective implements Validator { @Input() forbiddenName: string; - private valFn = Validators.nullValidator; - - ngOnChanges(changes: SimpleChanges): void { - const change = changes['forbiddenName']; - if (change) { - const val: string | RegExp = change.currentValue; - const re = val instanceof RegExp ? val : new RegExp(val, 'i'); - this.valFn = forbiddenNameValidator(re); - } else { - this.valFn = Validators.nullValidator; - } - } validate(control: AbstractControl): {[key: string]: any} { - return this.valFn(control); + return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control) + : null; } } // #enddocregion directive diff --git a/aio/content/examples/form-validation/src/app/shared/hero.ts b/aio/content/examples/form-validation/src/app/shared/hero.ts deleted file mode 100644 index fe2b55e51a..0000000000 --- a/aio/content/examples/form-validation/src/app/shared/hero.ts +++ /dev/null @@ -1,9 +0,0 @@ -// #docregion -export class Hero { - constructor( - public id: number, - public name: string, - public power: string, - public alterEgo?: string - ) { } -} diff --git a/aio/content/examples/form-validation/src/app/shared/shared.module.ts b/aio/content/examples/form-validation/src/app/shared/shared.module.ts deleted file mode 100644 index 2b0ada59bd..0000000000 --- a/aio/content/examples/form-validation/src/app/shared/shared.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -// #docregion -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -import { ForbiddenValidatorDirective } from './forbidden-name.directive'; -import { SubmittedComponent } from './submitted.component'; - -@NgModule({ - imports: [ CommonModule], - declarations: [ ForbiddenValidatorDirective, SubmittedComponent ], - exports: [ ForbiddenValidatorDirective, SubmittedComponent, - CommonModule ] -}) -export class SharedModule { } diff --git a/aio/content/examples/form-validation/src/app/shared/submitted.component.ts b/aio/content/examples/form-validation/src/app/shared/submitted.component.ts deleted file mode 100644 index 18cea6563f..0000000000 --- a/aio/content/examples/form-validation/src/app/shared/submitted.component.ts +++ /dev/null @@ -1,32 +0,0 @@ -// #docregion -import { Component, EventEmitter, Input, Output } from '@angular/core'; - -import { Hero } from './hero'; - -@Component({ - selector: 'hero-submitted', - template: ` -
-

You submitted the following:

-
-
Name
-
{{ hero.name }}
-
-
-
Alter Ego
-
{{ hero.alterEgo }}
-
-
-
Power
-
{{ hero.power }}
-
-
- -
` -}) -export class SubmittedComponent { - @Input() hero: Hero; - @Input() submitted = false; - @Output() submittedChange = new EventEmitter(); - onClick() { this.submittedChange.emit(false); } -} diff --git a/aio/content/examples/form-validation/src/app/template/hero-form-template.component.html b/aio/content/examples/form-validation/src/app/template/hero-form-template.component.html new file mode 100644 index 0000000000..c11336a3f2 --- /dev/null +++ b/aio/content/examples/form-validation/src/app/template/hero-form-template.component.html @@ -0,0 +1,66 @@ + +
+ +

Template-Driven Form

+ +
+ +
+ +
+ + + + + + +
+ +
+ Name is required. +
+
+ Name must be at least 4 characters long. +
+
+ Name cannot be Bob. +
+ +
+ +
+ +
+ + +
+ +
+ + + +
+
Power is required.
+
+
+ + + +
+ + +
+ +
diff --git a/aio/content/examples/form-validation/src/app/template/hero-form-template.component.ts b/aio/content/examples/form-validation/src/app/template/hero-form-template.component.ts new file mode 100644 index 0000000000..2ee1133eae --- /dev/null +++ b/aio/content/examples/form-validation/src/app/template/hero-form-template.component.ts @@ -0,0 +1,16 @@ +/* tslint:disable: member-ordering */ +// #docplaster +// #docregion +import { Component } from '@angular/core'; + +@Component({ + selector: 'hero-form-template', + templateUrl: './hero-form-template.component.html' +}) +export class HeroFormTemplateComponent { + + powers = ['Really Smart', 'Super Flexible', 'Weather Changer']; + + hero = {name: 'Dr.', alterEgo: 'Dr. What', power: this.powers[0]}; + +} diff --git a/aio/content/examples/form-validation/src/app/template/hero-form-template.module.ts b/aio/content/examples/form-validation/src/app/template/hero-form-template.module.ts deleted file mode 100644 index 042c019d5e..0000000000 --- a/aio/content/examples/form-validation/src/app/template/hero-form-template.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -// #docregion -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; - -import { SharedModule } from '../shared/shared.module'; -import { HeroFormTemplate1Component } from './hero-form-template1.component'; -import { HeroFormTemplate2Component } from './hero-form-template2.component'; - -@NgModule({ - imports: [ SharedModule, FormsModule ], - declarations: [ HeroFormTemplate1Component, HeroFormTemplate2Component ], - exports: [ HeroFormTemplate1Component, HeroFormTemplate2Component ] -}) -export class HeroFormTemplateModule { } diff --git a/aio/content/examples/form-validation/src/app/template/hero-form-template1.component.html b/aio/content/examples/form-validation/src/app/template/hero-form-template1.component.html deleted file mode 100644 index 22b374b622..0000000000 --- a/aio/content/examples/form-validation/src/app/template/hero-form-template1.component.html +++ /dev/null @@ -1,61 +0,0 @@ - -
-
-

Hero Form 1 (Template)

- -
- -
- - - - - -
-
- Name is required -
-
- Name must be at least 4 characters long. -
-
- Name cannot be more than 24 characters long. -
-
- -
- -
- - -
- -
- - - -
-
Power is required
-
-
- - - -
-
- - -
diff --git a/aio/content/examples/form-validation/src/app/template/hero-form-template1.component.ts b/aio/content/examples/form-validation/src/app/template/hero-form-template1.component.ts deleted file mode 100644 index 1bc29db44d..0000000000 --- a/aio/content/examples/form-validation/src/app/template/hero-form-template1.component.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* tslint:disable: member-ordering */ -// #docplaster -// #docregion -import { Component } from '@angular/core'; - - -import { Hero } from '../shared/hero'; - -@Component({ - selector: 'hero-form-template1', - templateUrl: './hero-form-template1.component.html' -}) -// #docregion class -export class HeroFormTemplate1Component { - - powers = ['Really Smart', 'Super Flexible', 'Weather Changer']; - - hero = new Hero(18, 'Dr. WhatIsHisWayTooLongName', this.powers[0], 'Dr. What'); - - submitted = false; - - onSubmit() { - this.submitted = true; - } -// #enddocregion class -// #enddocregion - // Reset the form with a new hero AND restore 'pristine' class state - // by toggling 'active' flag which causes the form - // to be removed/re-added in a tick via NgIf - // TODO: Workaround until NgForm has a reset method (#6822) - active = true; -// #docregion -// #docregion class - - addHero() { - this.hero = new Hero(42, '', ''); -// #enddocregion class -// #enddocregion - - this.active = false; - setTimeout(() => this.active = true, 0); -// #docregion -// #docregion class - } -} -// #enddocregion class -// #enddocregion diff --git a/aio/content/examples/form-validation/src/app/template/hero-form-template2.component.html b/aio/content/examples/form-validation/src/app/template/hero-form-template2.component.html deleted file mode 100644 index 8bb7066541..0000000000 --- a/aio/content/examples/form-validation/src/app/template/hero-form-template2.component.html +++ /dev/null @@ -1,52 +0,0 @@ - -
-
-

Hero Form 2 (Template & Messages)

- -
- -
- - - - - - - -
- {{ formErrors.name }} -
- -
- -
- - -
- -
- - - -
- {{ formErrors.power }} -
-
- - - -
-
- - -
diff --git a/aio/content/examples/form-validation/src/app/template/hero-form-template2.component.ts b/aio/content/examples/form-validation/src/app/template/hero-form-template2.component.ts deleted file mode 100644 index 320ef09efd..0000000000 --- a/aio/content/examples/form-validation/src/app/template/hero-form-template2.component.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* tslint:disable: member-ordering forin */ -// #docplaster -// #docregion -import { Component, AfterViewChecked, ViewChild } from '@angular/core'; -import { NgForm } from '@angular/forms'; - -import { Hero } from '../shared/hero'; - -@Component({ - selector: 'hero-form-template2', - templateUrl: './hero-form-template2.component.html' -}) -export class HeroFormTemplate2Component implements AfterViewChecked { - - powers = ['Really Smart', 'Super Flexible', 'Weather Changer']; - - hero = new Hero(18, 'Dr. WhatIsHisWayTooLongName', this.powers[0], 'Dr. What'); - - submitted = false; - - onSubmit() { - this.submitted = true; - } -// #enddocregion - - // Reset the form with a new hero AND restore 'pristine' class state - // by toggling 'active' flag which causes the form - // to be removed/re-added in a tick via NgIf - // TODO: Workaround until NgForm has a reset method (#6822) - active = true; -// #docregion - - addHero() { - this.hero = new Hero(42, '', ''); -// #enddocregion - - this.active = false; - setTimeout(() => this.active = true, 0); -// #docregion - } - - // #docregion view-child - heroForm: NgForm; - @ViewChild('heroForm') currentForm: NgForm; - - ngAfterViewChecked() { - this.formChanged(); - } - - formChanged() { - if (this.currentForm === this.heroForm) { return; } - this.heroForm = this.currentForm; - if (this.heroForm) { - this.heroForm.valueChanges - .subscribe(data => this.onValueChanged(data)); - } - } - // #enddocregion view-child - - // #docregion handler - onValueChanged(data?: any) { - if (!this.heroForm) { return; } - const form = this.heroForm.form; - - for (const field in this.formErrors) { - // clear previous error message (if any) - this.formErrors[field] = ''; - const control = form.get(field); - - if (control && control.dirty && !control.valid) { - const messages = this.validationMessages[field]; - for (const key in control.errors) { - this.formErrors[field] += messages[key] + ' '; - } - } - } - } - - formErrors = { - 'name': '', - 'power': '' - }; - // #enddocregion handler - - // #docregion messages - validationMessages = { - 'name': { - 'required': 'Name is required.', - 'minlength': 'Name must be at least 4 characters long.', - 'maxlength': 'Name cannot be more than 24 characters long.', - 'forbiddenName': 'Someone named "Bob" cannot be a hero.' - }, - 'power': { - 'required': 'Power is required.' - } - }; - // #enddocregion messages -} -// #enddocregion diff --git a/aio/content/examples/form-validation/src/forms.css b/aio/content/examples/form-validation/src/forms.css index 67ad13037b..1d388f656d 100644 --- a/aio/content/examples/form-validation/src/forms.css +++ b/aio/content/examples/form-validation/src/forms.css @@ -1,3 +1,4 @@ + .ng-valid[required], .ng-valid.required { border-left: 5px solid #42A948; /* green */ } diff --git a/aio/content/guide/form-validation.md b/aio/content/guide/form-validation.md index 140fed61f5..90c07ec89b 100644 --- a/aio/content/guide/form-validation.md +++ b/aio/content/guide/form-validation.md @@ -10,15 +10,16 @@ Improve overall data quality by validating user input for accuracy and completen 我们可以通过验证用户输入的准确性和完整性,来增强整体数据质量。 This page shows how to validate user input in the UI and display useful validation messages -using first the Template Driven Forms and then the Reactive Forms approach. +using both reactive and template-driven forms. It assumes some basic knowledge of the two +forms modules. 在本烹饪书中,我们展示在界面中如何验证用户输入,并显示有用的验证信息,先使用模板驱动表单方式,再使用响应式表单方式。
-Read more about these choices in the [Forms](guide/forms) -and the [Reactive Forms](guide/reactive-forms) guides. +If you're new to forms, start by reviewing the [Forms](guide/forms) and +[Reactive Forms](guide/reactive-forms) guides. 参见[表单](guide/forms)和[响应式表单](guide/reactive-forms)了解关于这些选择的更多知识。 @@ -26,102 +27,58 @@ and the [Reactive Forms](guide/reactive-forms) guides.
-{@a live-example} +## Template-driven validation +## 模板驱动验证 -**Try the live example to see and download the full cookbook source code.** +To add validation to a template-driven form, you add the same validation attributes as you +would with [native HTML form validation](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation). +Angular uses directives to match these attributes with validator functions in the framework. -**查看在线例子,并下载整个烹饪书的源代码** +为了往模板驱动表单中添加验证机制,我们要添加一些验证属性,就像[原生的HTML表单验证器](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation)。 +Angular 会用指令来匹配这些具有验证功能的指令。 +Every time the value of a form control changes, Angular runs validation and generates +either a list of validation errors, which results in an INVALID status, or null, which results in a VALID status. - - 在线例子 - +每当表单控件中的值发生变化时,Angular 就会进行验证,并生成一个验证错误的列表(对应着INVALID状态)或者null(对应着VALID状态)。 -## Simple Template Driven Forms +You can then inspect the control's state by exporting `ngModel` to a local template variable. +The following example exports `NgModel` into a variable called `name`: -## 简单的模板驱动表单 +我们可以通过把`ngModel`导出成局部模板变量来查看该控件的状态。 +比如下面这个例子就把`NgModel`导出成了一个名叫`name`的变量: -In the Template Driven approach, you arrange -[form elements](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Forms_in_HTML) in the component's template. - -在模板驱动表单方法中,你在组件的模板中组织[表单元素](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Forms_in_HTML)。 - -You add Angular form directives (mostly directives beginning `ng...`) to help -Angular construct a corresponding internal control model that implements form functionality. -In Template Driven forms, the control model is _implicit_ in the template. - -你可以添加Angular表单指令(通常为以`ng`开头的指令)来帮助Angular构建对应的内部控制模型,以实现表单功能。 -控制模型在模板中是**隐式**的。 - -To validate user input, you add [HTML validation attributes](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation) -to the elements. Angular interprets those as well, adding validator functions to the control model. - -要验证用户输入,你添加[HTML验证属性](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation)到元素中。 -Angular拦截这些元素,添加验证器函数到控制模型中。 - -Angular exposes information about the state of the controls including -whether the user has "touched" the control or made changes and if the control values are valid. - -Angular暴露关于控制状态的信息,包括用户是否已经“触摸“了控制器,或者用户已经作了更新和控制器的值是否还有效。 - -In this first template validation example, -notice the HTML that reads the control state and updates the display appropriately. -Here's an excerpt from the template HTML for a single input control bound to the hero name: - -在第一个模板验证例子中,我们添加了更多HTML,来读取控制器状态并适当更新显示。 -下面是模板HTML中提取的,一个绑定到英雄名字的输入框控制器: - - + - Note the following: 请注意以下几点: -* The `` element carries the HTML validation attributes: `required`, `minlength`, and `maxlength`. +* The `` element carries the HTML validation attributes: `required` and `minlength`. It +also carries a custom validator directive, `forbiddenName`. For more +information, see [Custom validators](guide/form-validation#custom-validators) section. - ``元素带有一些HTML验证属性:`required`、`minlength` 和 `maxlength`。 + ``元素带有一些HTML验证属性:`required`、`minlength` 和 `maxlength`。它还带有一个自定义的验证器指令`forbiddenName`。要了解更多信息,参见[自定义验证器](guide/form-validation#custom-validators)一节。 -* The `name` attribute of the input is set to `"name"` so Angular can track this input element and associate it -with an Angular form control called `name` in its internal control model. +* `#name="ngModel"` exports `NgModel` into a local variable callled `name`. `NgModel` mirrors many of the properties of its underlying +`FormControl` instance, so you can use this in the template to check for control states such as `valid` and `dirty`. For a full list of control properties, see the [AbstractControl](api/forms/AbstractControl) +API reference. - 我们把输入框的`name`属性设置为`"name"`,这样Angular可以跟踪这个输入元素,并将其内部控制器模型的一个名为`name`的Angular表单控制关联起来。 - - -* The `[(ngModel)]` directive allows two-way data binding between the input box to the `hero.name` property. - - 我们使用`[(ngModel)]`指令,将输入框双向数据绑定到`hero.name`属性。 - -* The template variable (`#name`) has the value `"ngModel"` (always `ngModel`). -This gives you a reference to the Angular `NgModel` directive -associated with this control that you can use _in the template_ -to check for control states such as `valid` and `dirty`. - - 我们将模板变量(`#name`)赋值为`"ngModel"` (总是 `ngModel`)。 - 它为我们提供了与这个控制器关联的Angular `NgModel`指令的引用,我们在模板中使用它,以检查控制器状态,比如`valid`和`dirty`。 + `#name="ngModel"`把`NgModel`导出成了一个名叫`name`的局部变量。`NgModel`把自己控制的`FormControl`实例的属性映射出去,让我们能在模板中检查控件的状态,比如`valid`和`dirty`。要了解完整的控件属性,参见 API 参考手册中的[AbstractControl](api/forms/AbstractControl)。 * The `*ngIf` on the `
` element reveals a set of nested message `divs` -but only if there are `name` errors and -the control is either `dirty` or `touched`. +but only if the `name` is invalid and the control is either `dirty` or `touched`. `
`元素的`*ngIf`揭露了一套嵌套消息`divs`,但是只在有“name”错误和控制器为`dirty`或者`touched`。 * Each nested `
` can present a custom message for one of the possible validation errors. -There are messages for `required`, `minlength`, and `maxlength`. - - 每个嵌套的`
`为其中一个可能出现的验证错误显示一条自定义消息。我们已经为`required`、`minlength`、和 `maxlength`准备了消息。 - -The full template repeats this kind of layout for each data entry control on the form. - -整个模板为表单上的每种数据输入控制器重复这种布局。 - - -{@a why-check} - +There are messages for `required`, `minlength`, and `forbiddenName`. + + 每个嵌套的`
`为其中一个可能出现的验证错误显示一条自定义消息。比如 `required`、`minlength`和 `forbiddenName`。
@@ -131,864 +88,240 @@ The full template repeats this kind of layout for each data entry control on the #### 为何检查**dirty**和**touched**? -The app shouldn't show errors for a new hero before the user has had a chance to edit the value. -The checks for `dirty` and `touched` prevent premature display of errors. -当用户创建一个新英雄时,在还没有机会输入之前,我们不应该显示任何错误。 -检查`dirty`和`touched`防止了这种过早的错误显示。 +You may not want your application to display errors before the user has a chance to edit the form. +The checks for `dirty` and `touched` prevent errors from showing until the user +does one of two things: changes the value, +turning the control dirty; or blurs the form control element, setting the control to touched. -Learn about `dirty` and `touched` in the [Forms](guide/forms) guide. - -参见[表单](guide/forms)章,学习关于`dirty`和`touched`的知识。 +我们肯定不希望应用在用户还没有编辑过表单的时候就给他们显示错误提示。 +对`dirty`和`touched`的检查可以避免这种问题。改变控件的值会改变控件的`dirty`(脏)状态,而当控件失去焦点时,就会改变控件的`touched`(碰过)状态。
+## Reactive form validation +## 响应式表单的验证 -The component class manages the hero model used in the data binding -as well as other code to support the view. +In a reactive form, the source of truth is the component class. Instead of adding validators through attributes in the template, you add validator functions directly to the form control model in the component class. Angular then calls these functions whenever the value of the control changes. -组件类管理用于数据绑定的英雄模型,它还有其他支持视图的代码。 +在响应式表单中,真正的源码都在组件类中。我们不应该通过模板上的属性来添加验证器,而应该在组件类中直接把验证器函数添加到表单控件模型上(`FormControl`)。然后,一旦控件发生了变化,Angular 就会调用这些函数。 +### Validator functions - +### 验证器函数 - +There are two types of validator functions: sync validators and async validators. +有两种验证器函数:同步验证器和异步验证器。 +* **Sync validators**: functions that take a control instance and immediately return either a set of validation errors or `null`. You can pass these in as the second argument when you instantiate a `FormControl`. -Use this Template Driven validation technique when working with static forms with simple, standard validation rules. + **同步验证器**函数接受一个控件实例,然后返回一组验证错误或`null`。我们可以在实例化一个`FormControl`时把它作为构造函数的第二个参数传进去。 -在处理简单的、拥有标准验证规则的静态表单时,使用这种模板驱动验证方法。 +* **Async validators**: functions that take a control instance and return a Promise +or Observable that later emits a set of validation errors or `null`. You can +pass these in as the third argument when you instantiate a `FormControl`. -Here are the complete files for the first version of `HeroFormTemplateCompononent` in the Template Driven approach: + **异步验证器**函数接受一个控件实例,并返回一个承诺(Promise)或可观察对象(Observable),它们稍后会发出一组验证错误或者`null`。我们可以在实例化一个`FormControl`时把它作为构造函数的第三个参数传进去。 -下面是第一个版本的使用模板驱动方法的`HeroFormTemplateComponent`: +Note: for performance reasons, Angular only runs async validators if all sync validators pass. Each must complete before errors are set. +注意:出于性能方面的考虑,只有在所有同步验证器都通过之后,Angular 才会运行异步验证器。当每一个异步验证器都执行完之后,才会设置这些验证错误。 - +### Built-in validators - +### 内置验证器 - +You can choose to [write your own validator functions](guide/form-validation#custom-validators), or you can use some of +Angular's built-in validators. - +我们可以[写自己的验证器](guide/form-validation#custom-validators),也可以使用一些 Angular 内置的验证器。 - +The same built-in validators that are available as attributes in template-driven forms, such as `required` and `minlength`, are all available to use as functions from the `Validators` class. For a full list of built-in validators, see the [Validators](api/forms/Validators) API reference. - - - - - - -## Template Driven Forms with validation messages in code - -## 验证消息在代码中的模板驱动表单 - -While the layout is straightforward, -there are obvious shortcomings with the way it's handling validation messages: - -虽然布局很直观,但是我们处理验证消息的方法有明显的缺陷: - -* It takes a lot of HTML to represent all possible error conditions. -This gets out of hand when there are many controls and many validation rules. - - 它使用了很多HTML来表现所有可能出现的错误情况。 - 如果有太多控制器和太多验证规则,我们就失去了控制。 - -* There's a lot of JavaScript logic in theHTML. - - 我们不喜欢在HTML里面插入这么多JavaScript。 - -* The messages are static strings, hard-coded into the template. -It's easier to maintain _dynamic_ messages in the component class. - - 这些消息是静态的字符串,被硬编码到模板中。把这些动态消息放在代码中会更易于维护。 - -In this example, you can move the logic and the messages into the component with a few changes to -the template and component. - -只需要对模板和组件做出一些修改,我们可以将逻辑和消息移到组件中。 - -Here's the hero name again, excerpted from the revised template (template 2), next to the original version: - -下面也是关于英雄名字的控制器,从修改后的模板(“Template 2”)中抽取出来,与原来的版本相比: - - - - - - - - - - - - - - - -The `` element HTML is almost the same. There are noteworthy differences: - -``元素的HTML几乎一样。但是下列有值得注意的区别: - -* The hard-code error message `` are gone. - - 硬编码的错误消息`
`消失了。 - -* There's a new attribute, `forbiddenName`, that is actually a custom validation directive. -It invalidates the control if the user enters "bob" in the name ``([try it](guide/form-validation#live-example)). -See the [custom validation](guide/form-validation#custom-validation) section later in this page for more information -on custom validation directives. - - 添加了一个新属性`forbiddenName`,它实际上是一个自定义验证指令。 - 如果用户名``中的任何地方输入“bob”,该指令变将控制器标记为无效([试试](guide/form-validation#live-example))。 - 我们将在本烹饪书稍后的地方介绍[自定义验证指令](guide/form-validation#custom-validation)。 - -* The `#name` template variable is gone because the app no longer refers to the Angular control for this element. - -模板变量`#name`消失了,因为我们不再需要为这个元素引用Angular控制器。 - -* Binding to the new `formErrors.name` property is sufficient to display all name validation error messages. - - 绑定到新的`formErrors.name`属性,就可以处理所有名字验证错误信息了。 - -{@a component-class} - -### Component class - -### 组件类 - -The original component code for Template 1 stayed the same; however, -Template 2 requires some changes in the component. This section covers the code -necessary in Template 2's component class to acquire the Angular -form control and compose error messages. - -原组件代码的模板一没变化,只是模板二发生了变化。本节包括模板二的组件类,以获取Angular表单控制器和撰写错误信息。 - -The first step is to acquire the form control that Angular created from the template by querying for it. - -第一步是获取Angular通过查询模板而生成的表单控制器。 - -Look back at the top of the component template at the -`#heroForm` template variable in the `
` element: - -回头看组件模板顶部,我们在``元素中设置`#heroForm`模板变量: - - - - - - - - -The `heroForm` variable is a reference to the control model that Angular derived from the template. -Tell Angular to inject that model into the component class's `currentForm` property using a `@ViewChild` query: - -`heroFrom`变量是Angular从模板衍生出来的控制模型的引用。 -我们利用`@ViewChild`来告诉Angular注入这个模型到组件类的`currentForm`属性: - - - - - - - - -Some observations: - -一些细节: - -* Angular `@ViewChild` queries for a template variable when you pass it -the name of that variable as a string (`'heroForm'` in this case). - - Angular的`@ViewChild`使用传入的模板变量的字符串名字(这里是`'heroForm'`),来查询对应的模板变量。 - -* The `heroForm` object changes several times during the life of the component, most notably when you add a new hero. -Periodically inspecting it reveals these changes. - - `heroForm`对象在组件的生命周期内变化了好几次,最值得注意的是当我们添加一个新英雄时的变化。我们必须定期重新检测它。 - -* Angular calls the `ngAfterViewChecked()` [lifecycle hook method](guide/lifecycle-hooks#afterview) -when anything changes in the view. -That's the right time to see if there's a new `heroForm` object. - - 当视图有任何变化时,Angular调用`ngAfterViewChecked`[生命周期钩子方法](guide/lifecycle-hooks#afterview)。这是查看是否有新`heroForm`对象的最佳时机。 - -* When there _is_ a new `heroForm` model, `formChanged()` subscribes to its `valueChanges` _Observable_ property. -The `onValueChanged` handler looks for validation errors after every keystroke. - - 当出现新`heroForm`模型时,我们订阅它的`valueChanged`**可观察**属性。 - `onValueChanged`处理器在每次用户键入后查找验证错误。 - - - - - - - - -The `onValueChanged` handler interprets user data entry. -The `data` object passed into the handler contains the current element values. -The handler ignores them. Instead, it iterates over the fields of the component's `formErrors` object. - -`onValueChanged`处理器拦截用户数据输入。 -包含当前元素值得`data`对象被传入处理器。 -处理器忽略它们。相反,它迭代组件的`formErrors`对象。 - -The `formErrors` is a dictionary of the hero fields that have validation rules and their current error messages. -Only two hero properties have validation rules, `name` and `power`. -The messages are empty strings when the hero data are valid. - -`formErrors`是一个词典,包含了拥有验证规则和当前错误消息的英雄控件。 - 只有两个英雄属性有验证规则,`name`和`power`。 - 当英雄数据有效时,这些消息的值为空字符串。 - -For each field, the `onValueChanged` handler does the following: - -对于每个字段,这个`onValueChanged`处理器会做这些: - - * Clears the prior error message, if any. - - 清除以前的错误信息(如果有的话) - - * Acquires the field's corresponding Angular form control. - - 获取控件对应的Angular表单控制器 - - * If such a control exists _and_ it's been changed ("dirty") - _and_ it's invalid, the handler composes a consolidated error message for all of the control's errors. - - 如果这样的控制器存在,并且它被更新过(“dirty”),**并且**它无效,处理器就会为所有控制器的错误合成一条错误消息。 - -Next, the component needs some error messages—a set for each validated property with -one message per validation rule: - -很显然,我们需要一些错误消息,每个验证的属性都需要一套,每个验证规则需要一条消息: - - - - - - - - -Now every time the user makes a change, the `onValueChanged` handler checks for validation errors and produces messages accordingly. - -现在,每次用户作出变化时,`onValueChanged`处理器检查验证错误并按情况发出错误消息。 - - -{@a improvement} - - -### The benefits of messages in code - -### 在代码中写消息的优点 - -Clearly the template got substantially smaller while the component code got substantially larger. -It's not easy to see the benefit when there are just three fields and only two of them have validation rules. - -很显然,模板变得小多了,组件代码变得大多了。当只有三个控件并且其中只有两个有验证规则时,我们很难看出好处。 - -Consider what happens as the number of validated -fields and rules increases. -In general, HTML is harder to read and maintain than code. -The initial template was already large and threatening to get rapidly worse -with the addition of more validation message `
` elements. - -假设增加需要验证的控件和规则后会怎么样。 -通常,HTML比代码更难阅读和维护。 -初始的模板已经很大了,如果我们添加更多验证消息`
`,它会迅速变得更大。 - -After moving the validation messaging to the component, -the template grows more slowly and proportionally. -Each field has approximately the same number of lines no matter its number of validation rules. -The component also grows proportionally, at the rate of one line per validated field -and one line per validation message. - -将验证消息移到组件后,模板的增长变得更加缓慢,幅度也小一些。 -不管有多少个验证规则,每个控件的行数是差不多的。 -组件也按比例增长,每增加一个控件增加一行,每个验证消息一行。 - -Now that the messages are in code, you have more flexibility and can compose messages more efficiently. -You can refactor the messages out of the component, perhaps to a service class that retrieves them from the server. -In short, there are more opportunities to improve message handling now that text and logic have moved from template to code. - -现在消息在代码中,我们有更多的灵活度。我们更加智能的撰写消息。 -我们可以将消息重构出组件,比如到一个服务类,从服务端获取消息。 -简而言之,有很多机会增强消息处理,因为文本和逻辑都已经从模板移到代码中。 - - -{@a formmodule} - - -### _FormModule_ and Template Driven forms - -### _FormModule_ 和模板驱动表单 - -Angular has two different forms modules—`FormsModule` and -`ReactiveFormsModule`—that correspond with the -two approaches to form development. Both modules come -from the same `@angular/forms` library package. - -Angular有两种不同的表单模块 - `FormsModule`和`ReactiveFormsModule` - 它们与表单开发的两种方法对应。 - 两种模块都从同一个`@angular/forms`库。 - -You've been reviewing the Template Driven approach which requires the `FormsModule`. -Here's how you imported it in the `HeroFormTemplateModule`. - -我们一直在探讨**模板驱动**方法,它需要`FormsModule`。下面是如何在`HeroFormTemplateModule`中导入它: - - - - - - - - -
- - - -This guide hasn't talked about the `SharedModule` or its `SubmittedComponent` which appears at the bottom of every -form template in this cookbook. - -我们还没有讲`SharedModule`或者它的`SubmittedComponent`,它们出现在本烹饪书的每一个表单模板中。 - -They're not germane to the validation story. Look at the [live example](guide/form-validation#live-example) if you're interested. - -它们与表单验证没有紧密的关系。如果你感兴趣,参见[在线例子](guide/form-validation#live-example)。 - - -
- - - - -{@a reactive} - - -## Reactive Forms with validation in code - -## 在代码中验证响应式表单 - -In the Template Driven approach, you mark up the template with form elements, validation attributes, -and `ng...` directives from the Angular `FormsModule`. -At runtime, Angular interprets the template and derives its _form control model_. - -在模板驱动方法中,你在模板中标出表单元素、验证属性和Angular`FormsModule`中的`ng...`指令。 -在运行时间,Angular解释模板并从**表单控制器模型**衍生它。 - -**Reactive Forms** takes a different approach. -You create the form control model in code. You write the template with form elements -and `form...` directives from the Angular `ReactiveFormsModule`. -At runtime, Angular binds the template elements to your control model based on your instructions. - -**响应式表单**采用不同的方法。 -你在代码中创建表单控制器模型,并用表单元素和来自Angular `ReactiveFormsModule`中的`form...`指令来编写模板。 -在运行时间,Angular根据你的指示绑定模板元素到你的控制器模型。 - -This approach requires a bit more effort. *You have to write the control model and manage it*. - -这个方法需要做一些额外的工作。*你必须编写并管理控制器模型**。 - -This allows you to do the following: - -这可以让你: - -* Add, change, and remove validation functions on the fly. - - 随时添加、修改和删除验证函数 - -* Manipulate the control model dynamically from within the component. - - 在组件内动态操纵控制器模型 - -* [Test](guide/form-validation#testing-considerations) validation and control logic with isolated unit tests. - - 使用孤立单元测试来[测试](guide/form-validation#testing-considerations)验证和控制器逻辑 - -The following sample re-writes the hero form in Reactive Forms style. - -第三个烹饪书例子用**响应式表单**风格重新编写英雄表格。 - - -{@a reactive-forms-module} - - -### Switch to the _ReactiveFormsModule_ - -### 切换到_ReactiveFormsModule_ - -The Reactive Forms classes and directives come from the Angular `ReactiveFormsModule`, not the `FormsModule`. -The application module for the Reactive Forms feature in this sample looks like this: - -响应式表单类和指令来自于Angular的`ReactiveFormsModule`,不是`FormsModule`。 -本例中,应用模块的“响应式表单”特性是这样的: - - - - - - - - -The Reactive Forms feature module and component are in the `src/app/reactive` folder. -Focus on the `HeroFormReactiveComponent` there, starting with its template. - -“响应式表单”特性模块和组件在`app/reactive`目录。 -让我们关注那里的`HeroFormReactiveComponent`,先看它的模板。 - - -{@a reactive-component-template} - - -### Component template - -### 组件模板 - -Begin by changing the `` tag so that it binds the Angular `formGroup` directive in the template -to the `heroForm` property in the component class. -The `heroForm` is the control model that the component class builds and maintains. - -我们先修改``标签,让Angular的`formGroup`指令绑定到组件类的`heroForm`属性。 -`heroForm`是组件类创建和维护的控制器模型。 - - - - - - - - -Next, modify the template HTML elements to match the Reactive Forms style. -Here is the "name" portion of the template again, revised for Reactive Forms and compared with the Template Driven version: - -接下来,我们修改模板HTML元素,来匹配**响应式表单**样式。 -下面又是“name”部分的模板,响应式表单修改版本和模板驱动版本的比较: - - - - - - - - - - - - - - - - -Key changes are: - -关键变化: - -* The validation attributes are gone (except `required`) because -validating happens in code. - - 验证属性没有了(除了`required`),因为我们将在代码中验证。 - -* `required` remains, not for validation purposes (that's in the code), -but rather for css styling and accessibility. - - 保留`required`,不是为了验证的目的(验证在代码中),而是为了CSS样式和可访问性。 - - -
- -Currently, Reactive Forms doesn't add the `required` or `aria-required` -HTML validation attribute to the DOM element -when the control has the `required` validator function. - -未来版本的响应式表单将会在控制器有`required`验证器函数时,添加`required` HTML验证属性到DOM元素(也可能添加`aria-required`属性)。 - -Until then, apply the `required` attribute _and_ add the `Validator.required` function -to the control model, as you'll see below. - -在此之前,添加`required`属性**以及**添加`Validator.required`函数到控制器模型,像我们下面这样做: - - -
- - - -* The `formControlName` replaces the `name` attribute; it serves the same -purpose of correlating the input with the Angular form control. - - `formControlName`替换了`name`属性;它起到了关联输入框和Angular表单控制器的同样作用。 - -* The two-way `[(ngModel)]` binding is gone. -The reactive approach does not use data binding to move data into and out of the form controls. -That's all in code. - - 双向`[(ngModel)]`绑定消失了。 -响应式表单方法不使用数据绑定从表单控制器移入和移出数据。我们在代码中做这些。 +模板驱动表单中可用的那些属性型验证器(如`required`、`minlength`等)对应于`Validators`类中的同名函数。要想查看内置验证器的全列表,参见 API 参考手册中的[验证器](api/forms/Validators)部分。 +To update the hero form to be a reactive form, you can use some of the same +built-in validators—this time, in function form. See below: +要想把这个英雄表单改造成一个响应式表单,我们还是用那些内置验证器,但这次改为用它们的函数形态。 {@a reactive-component-class} - -### Component class - -### 组件类 - -The component class is now responsible for defining and managing the form control model. - -组件类现在负责定义和管理表单控制器模型。 - -Angular no longer derives the control model from the template so you can no longer query for it. -You can create the Angular form control model explicitly with the help of the `FormBuilder`class. - -Angular不再从模板衍生控制器模型,所以我们不能再查询它。 -我们利用`FormBuilder`来显式创建Angular表单控制器模型。 - -Here's the section of code devoted to that process, paired with the Template Driven code it replaces: - -下面是负责该进程的代码部分,与被它取代的模板驱动代码相比: - - - - - - - - - - - - - - - -* Inject `FormBuilder` in a constructor. - - 我们注入`FormBuilder`到构造函数中。 - -* Call a `buildForm` method in the `ngOnInit` [lifecycle hook method](guide/lifecycle-hooks#hooks-overview) -because that's when you'll have the hero data. Call it again in the `addHero` method. - - 我们在`ngOnInit`[生命周期钩子方法](guide/lifecycle-hooks#hooks-overview)中调用`buildForm`方法, - 因为这正是我们拥有英雄数据的时刻。我们将在`addHero`方法中再次调用它。 - - -
- - - -A real app would retrieve the hero asynchronously from a data service, a task best performed in the `ngOnInit` hook. - -真实的应用很可能从数据服务异步获取英雄,这个任务最好在`ngOnInit`生命周期钩子中进行。 - -
- - - -* The `buildForm` method uses the `FormBuilder`, `fb`, to declare the form control model. -Then it attaches the same `onValueChanged` handler (there's a one line difference) -to the form's `valueChanges` event and calls it immediately -to set error messages for the new control model. - - `buildForm`方法使用`FormBuilder`(`fb`)来声明表单控制器模型。 -然后它将相同的`onValueChanged`(有一行代码不一样)处理器附加到表单的`valueChanged`事件, -并立刻为新的控制器模型设置错误消息。 - -## Built-in validators - -## 内置验证器 - -Angular forms include a number of built-in validator functions, which are functions -that help you check common user input in forms. In addition to the built-in -validators covered here of `minlength`, `maxlength`, -and `required`, there are others such as `email` and `pattern` -for Reactive Forms. -For a full list of built-in validators, -see the [Validators](api/forms/Validators) API reference. - - -#### _FormBuilder_ declaration - -#### _FormBuilder_声明 - -The `FormBuilder` declaration object specifies the three controls of the sample's hero form. - -`FormBuilder`声明对象指定了本例英雄表单的三个控制器。 - -Each control spec is a control name with an array value. -The first array element is the current value of the corresponding hero field. -The optional second value is a validator function or an array of validator functions. - -每个控制器的设置都是控制器名字和数组值。 -第一个数组元素是英雄控件对应的当前值。 -第二个值(可选)是验证器函数或者验证器函数数组。 - -Most of the validator functions are stock validators provided by Angular as static methods of the `Validators` class. -Angular has stock validators that correspond to the standard HTML validation attributes. - -大多数验证器函数是Angular以`Validators`类的静态方法的形式提供的原装验证器。 -Angular有一些原装验证器,与标准HTML验证属性一一对应。 - -The `forbiddenName` validator on the `"name"` control is a custom validator, -discussed in a separate [section below](guide/form-validation#custom-validation). - -`"name"`控制器上的`forbiddenNames`验证器是自定义验证器,在下面单独的[小结](guide/form-validation#custom-validation)有所讨论。 - - -
- -Learn more about `FormBuilder` in the [Introduction to FormBuilder](guide/reactive-forms#formbuilder) section of Reactive Forms guide. - -到[响应式表单]的[FormBuilder介绍](guide/reactive-forms#formbuilder)部分,学习更多关于`FormBuilder`的知识。 - - -
- -#### Committing hero value changes - -#### 提交英雄值的更新 - -In two-way data binding, the user's changes flow automatically from the controls back to the data model properties. -A Reactive Forms component should not use data binding to -automatically update data model properties. -The developer decides _when and how_ to update the data model from control values. - -在双向数据绑定时,用户的修改自动从控制器流向数据模型属性。 -响应式表单不适用数据绑定来更新数据模型属性。 -开发者决定**何时**与**如何**从控制器的值更新数据模型。 - -This sample updates the model twice: - -本例更新模型两次: - -1. When the user submits the form. - - 当用户提交标单时 - -1. When the user adds a new hero. - - 当用户添加新英雄时 - -The `onSubmit()` method simply replaces the `hero` object with the combined values of the form: - -`onSubmit()`方法直接使用表单的值得合集来替换`hero`对象: - - + +Note that: -The `addHero()` method discards pending changes and creates a brand new `hero` model object. +注意 -`addHero()`方法放弃未处理的变化,并创建一个崭新的`hero`模型对象。 +* The name control sets up two built-in validators—`Validators.required` and `Validators.minLength(4)`—and one custom validator, `forbiddenNameValidator`. For more details see the [Custom validators](guide/form-validation#custom-validators) section in this guide. + + `name`控件设置了两个内置验证器:`Validators.required` 和 `Validators.minLength(4)`。要了解更多信息,参见本章的[自定义验证器](guide/form-validation#custom-validators)一节。 + +* As these validators are all sync validators, you pass them in as the second argument. + + 由于这些验证器都是同步验证器,因此我们要把它们作为第二个参数传进去。 + +* Support multiple validators by passing the functions in as an array. + + 可以通过把这些函数放进一个数组后传进去,可以支持多重验证器。 + +* This example adds a few getter methods. In a reactive form, you can always access any form control through the `get` method on its parent group, but sometimes it's useful to define getters as shorthands +for the template. + + 这个例子添加了一些getter方法。在响应式表单中,我们通常会通过它所属的控件组(FormGroup)的`get`方法来访问表单控件,但有时候为模板定义一些getter作为简短形式。 - +If you look at the template for the name input again, it is fairly similar to the template-driven example. + +如果我们到模板中找到name输入框,就会发现它和模板驱动的例子很相似。 + + +Key takeaways: +关键改动是: + + * The form no longer exports any directives, and instead uses the `name` getter defined in + the component class. + + 该表单不再导出任何指令,而是使用组件类中定义的`name`读取器。 + + * The `required` attribute is still present. While it's not necessary for validation purposes, + you may want to keep it in your template for CSS styling or accessibility reasons. -Then it calls `buildForm()` again which replaces the previous `heroForm` control model with a new one. -The `` tag's `[formGroup]` binding refreshes the page with the new control model. + `required`属性仍然存在,虽然验证不再需要它,但我们仍然在模板中保留它,以支持 CSS 样式或可访问性。 -然后它再次调用`buildForm`,用一个新对象替换了之前的`heroForm`控制器模型。 -``标签的`[formGroup]`绑定使用这个新的控制器模型更新页面。 +## Custom validators -Here's the complete reactive component file, compared to the two Template Driven component files. +## 自定义验证器 -下面是完整的响应式表单的组件文件,与两个模板驱动组件文件对比: +Since the built-in validators won't always match the exact use case of your application, sometimes you'll want to create a custom validator. - +由于内置验证器无法适用于所有应用场景,有时候我们还是得创建自定义验证器。 - - - - - - - - - - - - - - - - -
- -Run the [live example](guide/form-validation#live-example) to see how the reactive form behaves, -and to compare all of the files in this sample. -运行[在线例子](guide/form-validation#live-example),查看响应式表单是的行为,并与本章中的例子文件作比较。 - -
- -## Custom validation - -## 自定义验证 - -This cookbook sample has a custom `forbiddenNameValidator()` function that's applied to both the -Template Driven and the reactive form controls. It's in the `src/app/shared` folder -and declared in the `SharedModule`. - -本烹饪书例子有一个自定义`forbiddenNameValidator`函数,在模板驱动和响应式表单中都有使用。 -它在`app/shared`目录,在`SharedModule`中被声明。 - -Here's the `forbiddenNameValidator()` function: - -下面是`forbiddenNameValidator`函数: +Consider the `forbiddenNameValidator` function from previous +[examples](guide/form-validation#reactive-component-class) in +this guide. Here's what the definition of that function looks like: +考虑前面的[例子](guide/form-validation#reactive-component-class)中的`forbiddenNameValidator`函数。该函数的定义看起来是这样的: +The function is actually a factory that takes a regular expression to detect a _specific_ forbidden name and returns a validator function. +这个函数实际上是一个工厂,它接受一个用来检测指定名字是否已被禁用的正则表达式,并返回一个验证器函数。 -The function is actually a factory that takes a regular expression to detect a _specific_ forbidden name -and returns a validator function. - -该函数其实是一个工厂函数,接受一个正则表达式,用来检测**指定**的禁止的名字,并返回验证器函数。 - -In this sample, the forbidden name is "bob"; -the validator rejects any hero name containing "bob". +In this sample, the forbidden name is "bob", so the validator will reject any hero name containing "bob". Elsewhere it could reject "alice" or any name that the configuring regular expression matches. 在本例中,禁止的名字是“bob”; -验证器拒绝任何带有“bob”的英雄名字。 +验证器会拒绝任何带有“bob”的英雄名字。 在其他地方,只要配置的正则表达式可以匹配上,它可能拒绝“alice”或者任何其他名字。 The `forbiddenNameValidator` factory returns the configured validator function. That function takes an Angular control object and returns _either_ null if the control value is valid _or_ a validation error object. The validation error object typically has a property whose name is the validation key, `'forbiddenName'`, -and whose value is an arbitrary dictionary of values that you could insert into an error message (`{name}`). +and whose value is an arbitrary dictionary of values that you could insert into an error message, `{name}`. `forbiddenNameValidator`工厂函数返回配置好的验证器函数。 该函数接受一个Angular控制器对象,并在控制器值有效时返回null,或无效时返回验证错误对象。 验证错误对象通常有一个名为验证秘钥(`forbiddenName`)的属性。其值为一个任意词典,我们可以用来插入错误信息(`{name}`)。 +### Adding to reactive forms +### 添加响应式表单 -### Custom validation directive +In reactive forms, custom validators are fairly simple to add. All you have to do is pass the function directly +to the `FormControl`. -### 自定义验证指令 +在响应式表单组件中,添加自定义验证器相当简单。你所要做的一切就是直接把这个函数传给 `FormControl` 。 -In the Reactive Forms component, the `'name'` control's validator function list -has a `forbiddenNameValidator` at the bottom. - -在响应式表单组件中,我们在`'name'`控制器的验证函数列表的底部添加了一个配置了的`forbiddenNameValidator`。 - - - + +### Adding to template-driven forms +In template-driven forms, you don't have direct access to the `FormControl` instance, so you can't pass the +validator in like you can for reactive forms. Instead, you need to add a directive to the template. -In the Template Driven example, the `` has the selector (`forbiddenName`) -of a custom _attribute directive_, which rejects "bob". +在模板驱动表单中,我们不用直接访问`FormControl`实例。所以我们不能像响应式表单中那样把验证器传进去,而应该在模板中添加一个指令。 -在模板驱动组件的模板中,我们在name的输入框元素中添加了自定义**属性指令**的选择器(`forbiddenName`),并配置它来拒绝“bob”。 +The corresponding `ForbiddenValidatorDirective` serves as a wrapper around the `forbiddenNameValidator`. +`ForbiddenValidatorDirective`指令相当于`forbiddenNameValidator`的包装器。 - - +Angular recognizes the directive's role in the validation process because the directive registers itself +with the `NG_VALIDATORS` provider, a provider with an extensible collection of validators. - - -The corresponding `ForbiddenValidatorDirective` is a wrapper around the `forbiddenNameValidator`. - -对应的`ForbiddenValidatorDirective`包装了`forbiddenNamevalidator`。 - -Angular `forms` recognizes the directive's role in the validation process because the directive registers itself -with the `NG_VALIDATORS` provider, a provider with an extensible collection of validation directives. - -Angular表单接受指令在验证流程中的作用,因为指令注册自己到`NG_VALIDATORS`提供商中,该提供商拥有可扩展的验证指令集。 +Angular在验证流程中的识别出指令的作用,是因为指令把自己注册到了`NG_VALIDATORS`提供商中,该提供商拥有一组可扩展的验证器。 +The directive class then implements the `Validator` interface, so that it can easily integrate +with Angular forms. Here is the rest of the directive to help you get an idea of how it all +comes together: - -Here is the rest of the directive to help you get an idea of how it all comes together: - -指令的其它部分是为了帮你理解它们是如何合作的: - +然后该指令类实现了`Validator`接口,以便它能简单的与 Angular 表单集成在一起。这个指令的其余部分有助于你理解它们是如何协作的: +Once the `ForbiddenValidatorDirective` is ready, you can simply add its selector, `forbiddenName`, to any input element to activate it. For example: +一旦 `ForbiddenValidatorDirective` 写好了,我们只要把`forbiddenName`选择器添加到输入框上就可以激活这个验证器了。比如: + + +
-If you are familiar with Angular validations, you may have noticed -that the custom validation directive is instantiated with `useExisting` +You may have noticed that the custom validation directive is instantiated with `useExisting` rather than `useClass`. The registered validator must be _this instance_ of the `ForbiddenValidatorDirective`—the instance in the form with its `forbiddenName` property bound to “bob". If you were to replace `useExisting` with `useClass`, then you’d be registering a new class instance, one that doesn’t have a `forbiddenName`. -如果你熟悉 Angular 的验证机制,可能会注意到自定义验证指令是使用`useExisting`而不是`useClass`来实例化的。这是因为注册的验证器必须是这个 `ForbiddenValidatorDirective` 实例本身,也就是表单中 `forbiddenName` 属性被绑定到了"bob"的那个。如果用`useClass`来代替`useExisting`,就会注册一个新的类实例,而它是没有`forbiddenName`的。 - -To see this in action, run the example and then type “bob” in the name of Hero Form 2. -Notice that you get a validation error. Now change from `useExisting` to `useClass` and try again. -This time, when you type “bob”, there's no "bob" error message. - -要查看它的运行效果,请打开范例,并在英雄表单2的 name 字段输入 "bob"。 -注意,我们会看到一个验证错误。现在,把 `useExisting` 改成 `useClass` 再试一下。这次,当你敲 "bob" 时,就不会再出现错误信息了。 +你可能注意到了自定义验证器指令是用`useExisting`而不是`useClass`来实例化的。注册的验证器必须是这个 `ForbiddenValidatorDirective` 实例本身,也就是表单中 `forbiddenName` 属性被绑定到了"bob"的那个。如果用`useClass`来代替`useExisting`,就会注册一个新的类实例,而它是没有`forbiddenName`的。
+## Control status CSS classes -
+## 表示控件状态的 CSS 类 -For more information on attaching behavior to elements, -see [Attribute Directives](guide/attribute-directives). +Like in AngularJS, Angular automatically mirrors many control properties onto the form control element as CSS classes. You can use these classes to style form control elements according to the state of the form. The following classes are currently supported: -参见[属性型指令](guide/attribute-directives)章节。 +像 AngularJS 中一样,Angular 会自动把很多控件属性作为 CSS 类映射到控件所在的元素上。我们可以使用这些类来根据表单状态给表单控件元素添加样式。目前支持下列类: + +* `.ng-valid` +* `.ng-invalid` +* `.ng-pending` +* `.ng-pristine` +* `.ng-dirty` +* `.ng-untouched` +* `.ng-touched` + +The hero form uses the `.ng-valid` and `.ng-invalid` classes to +set the color of each form control's border. + +这个英雄表单使用 `.ng-valid` 和 `.ng-invalid` 来设置每个表单控件的边框颜色。 + + + + -
+**You can run the to see the complete reactive and template-driven example code.** - - -## Testing Considerations - -## 测试时的注意事项 - -You can write _isolated unit tests_ of validation and control logic in Reactive Forms. - -我们可以为**响应式表单**的验证器和控制器逻辑编写**孤立单元测试**。 - -_Isolated unit tests_ probe the component class directly, independent of its -interactions with its template, the DOM, other dependencies, or Angular itself. - -**孤立单元测试**直接检测组件类,与组件和它的模板的交互、DOM、其他以来和Angular本省都无关。 - -Such tests have minimal setup, are quick to write, and easy to maintain. -They do not require the `Angular TestBed` or asynchronous testing practices. - -这样的测试具有简单设置#,快速编写和容易维护的特征。它们不需要`Angular TestBed`或异步测试工序。 - -That's not possible with Template Driven forms. -The Template Driven approach relies on Angular to produce the control model and -to derive validation rules from the HTML validation attributes. -You must use the `Angular TestBed` to create component test instances, -write asynchronous tests, and interact with the DOM. - -这对**模板驱动**表单来说是不可能的。 -模板驱动方法依靠Angular来生成控制器模型并从HTML验证属性中衍生验证规则。 -你必须使用`Angular TestBed`来创建组件测试实例,编写异步测试并与DOM交互。 - -While not difficult, this takes more time, work and -skill—factors that tend to diminish test code -coverage and quality. - -虽然这种测试并不困难,但是它需要更多时间、工作和能力 - 这些因素往往会降低测试代码覆盖率和测试质量。 +**你可以运行来查看完整的响应式和模板驱动表单的代码。** \ No newline at end of file diff --git a/aio/content/guide/i18n.md b/aio/content/guide/i18n.md index 0de99ae2bd..e7b0a33c06 100644 --- a/aio/content/guide/i18n.md +++ b/aio/content/guide/i18n.md @@ -642,7 +642,7 @@ This XML element represents the translation of the `

` greeting tag you marke 这个XML元素代表了你使用`i18n`属性标记的`

`问候语标签的翻译。
-Note that the translation unit `id=introductionHeader` is derived from the _custom_ `id`](#custom-id "Set a custom id") that you set earlier, but **without the `@@` prefix** required in the source HTML. +Note that the translation unit `id=introductionHeader` is derived from the [_custom_ `id`](#custom-id "Set a custom id") that you set earlier, but **without the `@@` prefix** required in the source HTML. 注意,翻译单元`id=introductionHeader`派生自[*自定义*`id`](#custom-id "设置自定义id"),它设置起来更简单,但是在HTML源码中**不需要`@@`前缀**。 diff --git a/aio/content/marketing/contributors.json b/aio/content/marketing/contributors.json index 0f67259285..1914caab35 100644 --- a/aio/content/marketing/contributors.json +++ b/aio/content/marketing/contributors.json @@ -342,7 +342,7 @@ "name": "Ralph Wang", "picture": "ralph.jpg", "twitter": "ralph_wang_gde", - "bio": "Ralph(Zhicheng Wang) is a senior consultant at ThoughWorks and also a GDE. He is a technology enthusiast and he is a passionate advocate of 'Simplicity, Professionalism and Sharing'. In his eighteen years of R&D career, he worked as tester, R&D engineer, project manager, product manager and CTO. He is looking forward to the birth of his baby.", + "bio": "Ralph(Zhicheng Wang) is a senior consultant at ThoughtWorks and also a GDE. He is a technology enthusiast and he is a passionate advocate of 'Simplicity, Professionalism and Sharing'. In his eighteen years of R&D career, he worked as tester, R&D engineer, project manager, product manager and CTO. He is immersed in the excitement of the arrival of the baby.", "group": "GDE" }, diff --git a/aio/content/marketing/index.html b/aio/content/marketing/index.html index a91dc4264b..452b1f6048 100755 --- a/aio/content/marketing/index.html +++ b/aio/content/marketing/index.html @@ -1,10 +1,10 @@ - +
- +
- +
@@ -12,31 +12,32 @@
- +
- - -
一套框架,多种平台。
移动端 & 桌面端
+
一套框架,多种平台
移动端 & 桌面端
快速上手 -
+

- + diff --git a/aio/content/navigation.json b/aio/content/navigation.json index 6a3ece8bce..a2f02e63c1 100644 --- a/aio/content/navigation.json +++ b/aio/content/navigation.json @@ -40,6 +40,12 @@ "hidden": true }, + { + "url": "guide/webpack", + "title": "Webpack: 简介", + "hidden": true + }, + { "url": "guide/quickstart", "title": "快速上手", @@ -165,7 +171,7 @@ { "url": "guide/forms", "title": "模板驱动表单", - "tooltip": "A form creates a cohesive, effective, and compelling data entry experience. An Angular form coordinates a set of data-bound user controls, tracks changes, validates input, and presents errors." + "tooltip": "表单可以创建集中、高效、引人注目的输入体验。Angular 表单可以协调一组数据绑定控件,跟踪变更,验证输入,并表达错误信息。" }, { "url": "guide/form-validation", @@ -381,7 +387,7 @@ "tooltip": "我们的联系方式、LOGO 和品牌" }, { - "url": "https://blog.angularjs.org/", + "url": "https://blog.angular.io/", "title": "博客", "tooltip": "Angular 官方博客" } diff --git a/aio/firebase.json b/aio/firebase.json index b4710b8c87..628a255a3a 100644 --- a/aio/firebase.json +++ b/aio/firebase.json @@ -6,26 +6,27 @@ "public": "dist", "cleanUrls": true, "redirects": [ - // cli-quickstart.html glossary.html, quickstart.html, http.html, style-guide.html, styleguide + // cli-quickstart.html, glossary.html, quickstart.html, server-communication.html, style-guide.html {"type": 301, "source": "/docs/ts/latest/cli-quickstart.html", "destination": "/guide/quickstart"}, - {"type": 301, "source": "/guide/cli-quickstart", "destination": "/guide/quickstart"}, {"type": 301, "source": "/docs/ts/latest/glossary.html", "destination": "/guide/glossary"}, {"type": 301, "source": "/docs/ts/latest/quickstart.html", "destination": "/guide/quickstart"}, {"type": 301, "source": "/docs/ts/latest/guide/server-communication.html", "destination": "/guide/http"}, {"type": 301, "source": "/docs/ts/latest/guide/style-guide.html", "destination": "/guide/styleguide"}, - {"type": 301, "source": "/styleguide", "destination": "/guide/styleguide"}, - // cookbook/component-communication.html + // guide/cli-quickstart, styleguide + {"type": 301, "source": "/guide/cli-quickstart", "destination": "/guide/quickstart"}, + {"type": 301, "source": "/styleguide", "destination": "/guide/styleguide"}, + + // cookbook/a1-a2-quick-reference.html, cookbook/component-communication.html, cookbook/dependency-injection.html + {"type": 301, "source": "/docs/ts/latest/cookbook/a1-a2-quick-reference.html", "destination": "/guide/ajs-quick-reference"}, {"type": 301, "source": "/docs/ts/latest/cookbook/component-communication.html", "destination": "/guide/component-interaction"}, + {"type": 301, "source": "/docs/ts/latest/cookbook/dependency-injection.html", "destination": "/guide/dependency-injection-in-action"}, // cookbook, cookbook/, cookbook/index.html {"type": 301, "source": "/docs/ts/latest/cookbook", "destination": "/docs"}, {"type": 301, "source": "/docs/ts/latest/cookbook/", "destination": "/docs"}, {"type": 301, "source": "/docs/ts/latest/cookbook/index.html", "destination": "/docs"}, - // cookbook/dependency-injection.html - {"type": 301, "source": "/docs/ts/latest/cookbook/dependency-injection.html", "destination": "/guide/dependency-injection-in-action"}, - // cookbook/*.html {"type": 301, "source": "/docs/ts/latest/cookbook/:cookbook.html", "destination": "/guide/:cookbook"}, diff --git a/aio/karma.conf.js b/aio/karma.conf.js index f89999269a..c2d83c7804 100644 --- a/aio/karma.conf.js +++ b/aio/karma.conf.js @@ -16,15 +16,8 @@ module.exports = function (config) { clearContext: false // leave Jasmine Spec Runner output visible in browser }, files: [ - { pattern: './node_modules/@angular/material/prebuilt-themes/indigo-pink.css', included: true }, - { pattern: './src/test.ts', watched: false } + { pattern: './node_modules/@angular/material/prebuilt-themes/indigo-pink.css', included: true } ], - preprocessors: { - './src/test.ts': ['@angular/cli'] - }, - mime: { - 'text/x-typescript': ['ts','tsx'] - }, coverageIstanbulReporter: { reports: [ 'html', 'lcovonly' ], fixWebpackSourcePaths: true @@ -32,14 +25,13 @@ module.exports = function (config) { angularCli: { environment: 'dev' }, - reporters: config.angularCli && config.angularCli.codeCoverage - ? ['progress', 'coverage-istanbul'] - : ['progress', 'kjhtml'], + reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], + browserNoActivityTimeout: 60000, singleRun: false }); }; diff --git a/aio/package.json b/aio/package.json index 6881103fdb..7309a5c78a 100644 --- a/aio/package.json +++ b/aio/package.json @@ -9,7 +9,7 @@ "ng": "yarn check-env && ng", "start": "yarn check-env && ng serve", "prebuild": "yarn check-env && yarn setup", - "build": "ng build -prod -sm -vc=false", + "build": "ng build --target=production --environment=stable -sm -bo", "postbuild": "yarn sw-manifest && yarn sw-copy", "lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint", "test": "yarn check-env && ng test", @@ -22,8 +22,7 @@ "example-e2e": "node ./tools/examples/run-example-e2e", "example-lint": "tslint -c \"content/examples/tslint.json\" \"content/examples/**/*.ts\" -e \"content/examples/styleguide/**/*.avoid.ts\"", "deploy-preview": "scripts/deploy-preview.sh", - "deploy-staging": "scripts/deploy-to-firebase.sh staging", - "deploy-production": "scripts/deploy-to-firebase.sh production", + "deploy-production": "scripts/deploy-to-firebase.sh", "check-env": "node scripts/check-environment", "payload-size": "scripts/payload.sh", "predocs": "rimraf src/generated/{docs,*.json}", @@ -31,6 +30,7 @@ "docs-watch": "node tools/transforms/authors-package/watchr.js", "docs-lint": "eslint --ignore-path=\"tools/transforms/.eslintignore\" tools/transforms", "docs-test": "node tools/transforms/test.js", + "tools-test": "./scripts/deploy-to-firebase.test.sh && yarn docs-test", "serve-and-sync": "concurrently --kill-others \"yarn docs-watch\" \"yarn start\"", "~~update-webdriver": "webdriver-manager update --standalone false --gecko false", "boilerplate:add": "node ./tools/examples/example-boilerplate add", @@ -40,7 +40,7 @@ "generate-zips": "node ./tools/example-zipper/generateZips", "sw-manifest": "ngu-sw-manifest --dist dist --in ngsw-manifest.json --out dist/ngsw-manifest.json", "sw-copy": "cp node_modules/@angular/service-worker/bundles/worker-basic.min.js dist/", - "postinstall": "node tools/cli-patches/patch.js && uglifyjs node_modules/lunr/lunr.js -c -m -o src/assets/js/lunr.min.js --source-map", + "postinstall": "uglifyjs node_modules/lunr/lunr.js -c -m -o src/assets/js/lunr.min.js --source-map", "build-ie-polyfills": "node node_modules/webpack/bin/webpack.js -p src/ie-polyfills.js src/generated/ie-polyfills.min.js" }, "engines": { @@ -66,14 +66,13 @@ "core-js": "^2.4.1", "jasmine": "^2.6.0", "ng-pwa-tools": "^0.0.10", - "ngo": "angular/ngo", "rxjs": "^5.2.0", "tslib": "^1.7.1", "web-animations-js": "^2.2.5", "zone.js": "^0.8.12" }, "devDependencies": { - "@angular/cli": "angular/cli-builds#webpack-next", + "@angular/cli": "1.3.0-rc.3", "@angular/compiler-cli": "^4.3.1", "@types/jasmine": "^2.5.52", "@types/node": "~6.0.60", diff --git a/aio/scripts/deploy-to-firebase.sh b/aio/scripts/deploy-to-firebase.sh index a14ba8965b..1542067de0 100755 --- a/aio/scripts/deploy-to-firebase.sh +++ b/aio/scripts/deploy-to-firebase.sh @@ -3,32 +3,82 @@ # WARNING: FIREBASE_TOKEN should NOT be printed. set +x -eu -o pipefail +# Only deploy if this not a PR. PRs are deployed early in `build.sh`. +if [[ $TRAVIS_PULL_REQUEST != "false" ]]; then + echo "Skipping deploy because this is a PR build." + exit 0 +fi -readonly deployEnv=$1 +# Do not deploy if the current commit is not the latest on its branch. +readonly LATEST_COMMIT=$(git ls-remote origin $TRAVIS_BRANCH | cut -c1-40) +if [[ $TRAVIS_COMMIT != $LATEST_COMMIT ]]; then + echo "Skipping deploy because $TRAVIS_COMMIT is not the latest commit ($LATEST_COMMIT)." + exit 0 +fi + +# The deployment mode is computed based on the branch we are building +if [[ $TRAVIS_BRANCH == master ]]; then + readonly deployEnv=next +elif [[ $TRAVIS_BRANCH == $STABLE_BRANCH ]]; then + readonly deployEnv=stable +else + # Extract the major versions from the branches, e.g. the 4 from 4.3.x + readonly majorVersion=${TRAVIS_BRANCH%%.*} + readonly majorVersionStable=${STABLE_BRANCH%%.*} + + # Do not deploy if the major version is not less than the stable branch major version + if [[ $majorVersion -ge $majorVersionStable ]]; then + echo "Skipping deploy of branch \"${TRAVIS_BRANCH}\" to firebase." + echo "We only deploy archive branches with the major version less than the stable branch: \"${STABLE_BRANCH}\"" + exit 0 + fi + + # Find the branch that has highest minor version for the given `$majorVersion` + readonly mostRecentMinorVersion=$( + # List the branches that start with the major version + git ls-remote origin refs/heads/${majorVersion}.*.x | + # Extract the version number + awk -F'/' '{print $3}' | + # Sort by the minor version + sort -t. -k 2,2n | + # Get the highest version + tail -n1 + ) + + # Do not deploy as it is not the latest branch for the given major version + if [[ $TRAVIS_BRANCH != $mostRecentMinorVersion ]]; then + echo "Skipping deploy of branch \"${TRAVIS_BRANCH}\" to firebase." + echo "There is a more recent branch with the same major version: \"${mostRecentMinorVersion}\"" + exit 0 + fi + + readonly deployEnv=archive +fi case $deployEnv in - staging) - readonly buildEnv=stage + next) readonly projectId=aio-staging - readonly deployedUrl=https://$projectId.firebaseapp.com/ + readonly deployedUrl=https://next.angular.io/ readonly firebaseToken=$FIREBASE_TOKEN ;; - production) - readonly buildEnv=prod + stable) readonly projectId=angular-io readonly deployedUrl=https://angular.io/ readonly firebaseToken=$FIREBASE_TOKEN ;; - *) - echo "Unknown deployment environment ('$deployEnv'). Expected 'staging' or 'production'." - exit 1 + archive) + readonly projectId=angular-io-${majorVersion} + readonly deployedUrl=https://v${majorVersion}.angular.io/ + readonly firebaseToken=$FIREBASE_TOKEN ;; esac -# Do not deploy if the current commit is not the latest on its branch. -readonly LATEST_COMMIT=$(git ls-remote origin $TRAVIS_BRANCH | cut -c1-40) -if [ $TRAVIS_COMMIT != $LATEST_COMMIT ]; then - echo "Skipping deploy because $TRAVIS_COMMIT is not the latest commit ($LATEST_COMMIT)." +echo "Git branch : $TRAVIS_BRANCH" +echo "Build/deploy mode : $deployEnv" +echo "Firebase project : $projectId" +echo "Deployment URL : $deployedUrl" + +if [[ $1 == "--dry-run" ]]; then exit 0 fi @@ -37,7 +87,10 @@ fi cd "`dirname $0`/.." # Build the app - yarn build -- --env=$buildEnv + yarn build -- --env=$deployEnv + + # Include any mode-specific files + cp -rf src/extra-files/$deployEnv/. dist/ # Check payload size yarn payload-size diff --git a/aio/scripts/deploy-to-firebase.test.sh b/aio/scripts/deploy-to-firebase.test.sh new file mode 100755 index 0000000000..dd9a3235fd --- /dev/null +++ b/aio/scripts/deploy-to-firebase.test.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash + +function check { + if [[ $1 == $2 ]]; then + echo Pass + exit 0 + fi + echo Fail + echo ---- Expected ---- + echo "$2" + echo ---- Actual ---- + echo "$1" + exit 1 +} + +( + echo ===== master - skip deploy - pull request + actual=$( + export TRAVIS_PULL_REQUEST=true + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Skipping deploy because this is a PR build." + check "$actual" "$expected" +) + +( + echo ===== master - deploy success + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=master + export TRAVIS_COMMIT=$(git ls-remote origin master | cut -c-40) + export FIREBASE_TOKEN=XXXXX + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Git branch : master +Build/deploy mode : next +Firebase project : aio-staging +Deployment URL : https://next.angular.io/" + check "$actual" "$expected" +) + +( + echo ===== master - skip deploy - commit not HEAD + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=master + export TRAVIS_COMMIT=DUMMY_TEST_COMMIT + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Skipping deploy because DUMMY_TEST_COMMIT is not the latest commit ($(git ls-remote origin master | cut -c1-40))." + check "$actual" "$expected" +) + +( + echo ===== stable - deploy success + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=4.3.x + export STABLE_BRANCH=4.3.x + export TRAVIS_COMMIT=$(git ls-remote origin 4.3.x | cut -c-40) + export FIREBASE_TOKEN=XXXXX + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Git branch : 4.3.x +Build/deploy mode : stable +Firebase project : angular-io +Deployment URL : https://angular.io/" + check "$actual" "$expected" +) + +( + echo ===== stable - skip deploy - commit not HEAD + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=4.3.x + export STABLE_BRANCH=4.3.x + export TRAVIS_COMMIT=DUMMY_TEST_COMMIT + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Skipping deploy because DUMMY_TEST_COMMIT is not the latest commit ($(git ls-remote origin 4.3.x | cut -c1-40))." + check "$actual" "$expected" +) + +( + echo ===== archive - deploy success + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=2.4.x + export STABLE_BRANCH=4.3.x + export TRAVIS_COMMIT=$(git ls-remote origin 2.4.x | cut -c-40) + export FIREBASE_TOKEN=XXXXX + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Git branch : 2.4.x +Build/deploy mode : archive +Firebase project : angular-io-2 +Deployment URL : https://v2.angular.io/" + check "$actual" "$expected" +) + +( + echo ===== archive - skip deploy - commit not HEAD + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=2.4.x + export STABLE_BRANCH=4.3.x + export TRAVIS_COMMIT=DUMMY_TEST_COMMIT + export FIREBASE_TOKEN=XXXXX + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Skipping deploy because DUMMY_TEST_COMMIT is not the latest commit ($(git ls-remote origin 2.4.x | cut -c1-40))." + check "$actual" "$expected" +) + +( + echo ===== archive - skip deploy - major version too high, lower minor + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=2.1.x + export STABLE_BRANCH=2.2.x + export TRAVIS_COMMIT=$(git ls-remote origin 2.1.x | cut -c-40) + export FIREBASE_TOKEN=XXXXX + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Skipping deploy of branch \"2.1.x\" to firebase. +We only deploy archive branches with the major version less than the stable branch: \"2.2.x\"" + check "$actual" "$expected" +) + +( + echo ===== archive - skip deploy - major version too high, higher minor + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=2.4.x + export STABLE_BRANCH=2.2.x + export TRAVIS_COMMIT=$(git ls-remote origin 2.1.x | cut -c-40) + export FIREBASE_TOKEN=XXXXX + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Skipping deploy of branch \"2.1.x\" to firebase. +We only deploy archive branches with the major version less than the stable branch: \"2.2.x\"" + check "$actual" "$expected" +) + +( + echo ===== archive - skip deploy - minor version too low + actual=$( + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=2.1.x + export STABLE_BRANCH=4.3.x + export TRAVIS_COMMIT=$(git ls-remote origin 2.1.x | cut -c-40) + export FIREBASE_TOKEN=XXXXX + `dirname $0`/deploy-to-firebase.sh --dry-run + ) + expected="Skipping deploy of branch \"2.1.x\" to firebase. +There is a more recent branch with the same major version: \"2.4.x\"" + check "$actual" "$expected" +) diff --git a/aio/scripts/payload.sh b/aio/scripts/payload.sh index 0dc0bd9c30..aacd7c615e 100755 --- a/aio/scripts/payload.sh +++ b/aio/scripts/payload.sh @@ -61,7 +61,8 @@ else # Nothing changed in aio/ exit 0 fi -payloadData="$payloadData\"change\": \"$change\"" +message=$(echo $TRAVIS_COMMIT_MESSAGE | sed 's/"/\\"/g' | sed 's/\\/\\\\/g') +payloadData="$payloadData\"change\": \"$change\", \"message\": \"$message\"" payloadData="{${payloadData}}" diff --git a/aio/src/app/app.component.html b/aio/src/app/app.component.html index 22127e12c2..8f42887b50 100644 --- a/aio/src/app/app.component.html +++ b/aio/src/app/app.component.html @@ -21,12 +21,13 @@ -
- +
+
+
diff --git a/aio/src/app/app.component.spec.ts b/aio/src/app/app.component.spec.ts index c69ffe6fdd..ea67b3bd6e 100644 --- a/aio/src/app/app.component.spec.ts +++ b/aio/src/app/app.component.spec.ts @@ -12,6 +12,7 @@ import { of } from 'rxjs/observable/of'; import { AppComponent } from './app.component'; import { AppModule } from './app.module'; import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component'; +import { Deployment } from 'app/shared/deployment.service'; import { GaService } from 'app/shared/ga.service'; import { LocationService } from 'app/shared/location.service'; import { Logger } from 'app/shared/logger.service'; @@ -273,26 +274,49 @@ describe('AppComponent', () => { describe('SideNav version selector', () => { let selectElement: DebugElement; let selectComponent: SelectComponent; - beforeEach(() => { + + function setupSelectorForTesting(mode?: string) { + createTestingModule('a/b', mode); + initializeTest(); component.onResize(sideBySideBreakPoint + 1); // side-by-side selectElement = fixture.debugElement.query(By.directive(SelectComponent)); selectComponent = selectElement.componentInstance; + } + + it('should select the version that matches the deploy mode', () => { + setupSelectorForTesting(); + expect(selectComponent.selected.title).toContain('stable'); + setupSelectorForTesting('next'); + expect(selectComponent.selected.title).toContain('next'); + setupSelectorForTesting('archive'); + expect(selectComponent.selected.title).toContain('v4'); }); - it('should pick first (current) version by default', () => { - expect(selectComponent.selected.title).toEqual(component.versionInfo.raw); + it('should add the current raw version string to the selected version', () => { + setupSelectorForTesting(); + expect(selectComponent.selected.title).toContain(`(v${component.versionInfo.raw})`); + setupSelectorForTesting('next'); + expect(selectComponent.selected.title).toContain(`(v${component.versionInfo.raw})`); + setupSelectorForTesting('archive'); + expect(selectComponent.selected.title).toContain(`(v${component.versionInfo.raw})`); }); // Older docs versions have an href - it('should navigate when change to a version with an href', () => { - selectElement.triggerEventHandler('change', { option: component.docVersions[1] as Option, index: 1}); - expect(locationService.go).toHaveBeenCalledWith(TestHttp.docVersions[0].url); + it('should navigate when change to a version with a url', () => { + setupSelectorForTesting(); + const versionWithUrlIndex = component.docVersions.findIndex(v => !!v.url); + const versionWithUrl = component.docVersions[versionWithUrlIndex]; + selectElement.triggerEventHandler('change', { option: versionWithUrl, index: versionWithUrlIndex}); + expect(locationService.go).toHaveBeenCalledWith(versionWithUrl.url); }); // The current docs version should not have an href // This may change when we perfect our docs versioning approach - it('should not navigate when change to a version without an href', () => { - selectElement.triggerEventHandler('change', { option: component.docVersions[0] as Option, index: 0}); + it('should not navigate when change to a version without a url', () => { + setupSelectorForTesting(); + const versionWithoutUrlIndex = component.docVersions.findIndex(v => !v.url); + const versionWithoutUrl = component.docVersions[versionWithoutUrlIndex]; + selectElement.triggerEventHandler('change', { option: versionWithoutUrl, index: versionWithoutUrlIndex}); expect(locationService.go).not.toHaveBeenCalled(); }); }); @@ -332,10 +356,6 @@ describe('AppComponent', () => { }); describe('hostClasses', () => { - let host: DebugElement; - beforeEach(() => { - host = fixture.debugElement; - }); it('should set the css classes of the host container based on the current doc and navigation view', () => { locationService.go('guide/pipes'); @@ -359,7 +379,7 @@ describe('AppComponent', () => { }); it('should set the css class of the host container based on the open/closed state of the side nav', () => { - const sideNav = host.query(By.directive(MdSidenav)); + const sideNav = fixture.debugElement.query(By.directive(MdSidenav)); locationService.go('guide/pipes'); fixture.detectChanges(); @@ -376,7 +396,14 @@ describe('AppComponent', () => { checkHostClass('sidenav', 'open'); }); + it('should set the css class of the host container based on the initial deployment mode', () => { + createTestingModule('a/b', 'archive'); + initializeTest(); + checkHostClass('mode', 'archive'); + }); + function checkHostClass(type, value) { + const host = fixture.debugElement; const classes = host.properties['className']; const classArray = classes.split(' ').filter(c => c.indexOf(`${type}-`) === 0); expect(classArray.length).toBeLessThanOrEqual(1, `"${classes}" should have only one class matching ${type}-*`); @@ -623,7 +650,25 @@ describe('AppComponent', () => { describe('footer', () => { it('should have version number', () => { const versionEl: HTMLElement = fixture.debugElement.query(By.css('aio-footer')).nativeElement; - expect(versionEl.textContent).toContain(TestHttp.versionFull); + expect(versionEl.textContent).toContain(TestHttp.versionInfo.full); + }); + }); + + describe('deployment banner', () => { + it('should show a message if the deployment mode is "archive"', () => { + createTestingModule('a/b', 'archive'); + initializeTest(); + fixture.detectChanges(); + const banner: HTMLElement = fixture.debugElement.query(By.css('aio-mode-banner')).nativeElement; + expect(banner.textContent).toContain('archived documentation for Angular v4'); + }); + + it('should show no message if the deployment mode is not "archive"', () => { + createTestingModule('a/b', 'stable'); + initializeTest(); + fixture.detectChanges(); + const banner: HTMLElement = fixture.debugElement.query(By.css('aio-mode-banner')).nativeElement; + expect(banner.textContent.trim()).toEqual(''); }); }); @@ -720,6 +765,97 @@ describe('AppComponent', () => { }); }); + describe('archive redirection', () => { + it('should redirect to `docs` if deployment mode is `archive` and not at a docs page', () => { + createTestingModule('', 'archive'); + initializeTest(); + expect(TestBed.get(LocationService).replace).toHaveBeenCalledWith('docs'); + + createTestingModule('resources', 'archive'); + initializeTest(); + expect(TestBed.get(LocationService).replace).toHaveBeenCalledWith('docs'); + + createTestingModule('guide/aot-compiler', 'archive'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('tutorial', 'archive'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('tutorial/toh-pt1', 'archive'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('docs', 'archive'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('api', 'archive'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + }); + + it('should redirect to `docs` if deployment mode is `next` and not at a docs page', () => { + createTestingModule('', 'next'); + initializeTest(); + expect(TestBed.get(LocationService).replace).toHaveBeenCalledWith('docs'); + + createTestingModule('resources', 'next'); + initializeTest(); + expect(TestBed.get(LocationService).replace).toHaveBeenCalledWith('docs'); + + createTestingModule('guide/aot-compiler', 'next'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('tutorial', 'next'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('tutorial/toh-pt1', 'next'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('docs', 'next'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('api', 'next'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + }); + + it('should not redirect to `docs` if deployment mode is `stable` and not at a docs page', () => { + createTestingModule('', 'stable'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('resources', 'stable'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('guide/aot-compiler', 'stable'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('tutorial', 'stable'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('tutorial/toh-pt1', 'stable'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('docs', 'stable'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + + createTestingModule('api', 'stable'); + initializeTest(); + expect(TestBed.get(LocationService).replace).not.toHaveBeenCalled(); + }); + }); }); describe('with mocked DocViewer', () => { @@ -883,7 +1019,8 @@ describe('AppComponent', () => { //// test helpers //// -function createTestingModule(initialUrl: string) { +function createTestingModule(initialUrl: string, mode: string = 'stable') { + const mockLocationService = new MockLocationService(initialUrl); TestBed.resetTestingModule(); TestBed.configureTestingModule({ imports: [ AppModule ], @@ -891,9 +1028,14 @@ function createTestingModule(initialUrl: string) { { provide: APP_BASE_HREF, useValue: '/' }, { provide: GaService, useClass: TestGaService }, { provide: Http, useClass: TestHttp }, - { provide: LocationService, useFactory: () => new MockLocationService(initialUrl) }, + { provide: LocationService, useFactory: () => mockLocationService }, { provide: Logger, useClass: MockLogger }, { provide: SearchService, useClass: MockSearchService }, + { provide: Deployment, useFactory: () => { + const deployment = new Deployment(mockLocationService as any); + deployment.mode = mode; + return deployment; + }}, ] }); } @@ -908,7 +1050,21 @@ class TestSearchService { } class TestHttp { - static versionFull = '4.0.0-local+sha.73808dd'; + + static versionInfo = { + raw: '4.0.0-rc.6', + major: 4, + minor: 0, + patch: 0, + prerelease: [ 'local' ], + build: 'sha.73808dd', + version: '4.0.0-local', + codeName: 'snapshot', + isSnapshot: true, + full: '4.0.0-local+sha.73808dd', + branch: 'master', + commitSHA: '73808dd38b5ccd729404936834d1568bd066de81' + }; static docVersions: NavigationNode[] = [ { title: 'v2', url: 'https://v2.angular.cn' } @@ -951,22 +1107,7 @@ class TestHttp { ], "docVersions": TestHttp.docVersions, - "__versionInfo": { - "raw": "4.0.0-rc.6", - "major": 4, - "minor": 0, - "patch": 0, - "prerelease": [ - "local" - ], - "build": "sha.73808dd", - "version": "4.0.0-local", - "codeName": "snapshot", - "isSnapshot": true, - "full": TestHttp.versionFull, - "branch": "master", - "commitSHA": "73808dd38b5ccd729404936834d1568bd066de81" - } + "__versionInfo": TestHttp.versionInfo, }; get(url: string) { diff --git a/aio/src/app/app.component.ts b/aio/src/app/app.component.ts index 3ba0293a4b..84c283ded8 100644 --- a/aio/src/app/app.component.ts +++ b/aio/src/app/app.component.ts @@ -5,6 +5,7 @@ import { MdSidenav } from '@angular/material'; import { CurrentNodes, NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service'; import { DocumentService, DocumentContents } from 'app/documents/document.service'; import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component'; +import { Deployment } from 'app/shared/deployment.service'; import { LocationService } from 'app/shared/location.service'; import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component'; import { ScrollService } from 'app/shared/scroll.service'; @@ -99,6 +100,7 @@ export class AppComponent implements OnInit { sidenav: MdSidenav; constructor( + public deployment: Deployment, private documentService: DocumentService, private hostElement: ElementRef, private locationService: LocationService, @@ -127,6 +129,11 @@ export class AppComponent implements OnInit { }); this.locationService.currentPath.subscribe(path => { + // Redirect to docs if we are in not in stable mode and are not hitting a docs page + // (i.e. we have arrived at a marketing page) + if (this.deployment.mode !== 'stable' && !/^(docs$|api$|guide|tutorial)/.test(path)) { + this.locationService.replace('docs'); + } if (path === this.currentPath) { // scroll only if on same page (most likely a change to the hash) this.autoScroll(); @@ -158,12 +165,24 @@ export class AppComponent implements OnInit { // Compute the version picker list from the current version and the versions in the navigation map combineLatest( - this.navigationService.versionInfo.map(versionInfo => ({ title: versionInfo.raw, url: null })), - this.navigationService.navigationViews.map(views => views['docVersions']), - (currentVersion, otherVersions) => [currentVersion, ...otherVersions]) - .subscribe(versions => { - this.docVersions = versions; - this.currentDocVersion = this.docVersions[0]; + this.navigationService.versionInfo, + this.navigationService.navigationViews.map(views => views['docVersions'])) + .subscribe(([versionInfo, versions]) => { + // TODO(pbd): consider whether we can lookup the stable and next versions from the internet + const computedVersions = [ + { title: 'next', url: 'https://next.angular.io' }, + { title: 'stable', url: 'https://angular.io' }, + ]; + if (this.deployment.mode === 'archive') { + computedVersions.push({ title: `v${versionInfo.major}`, url: null }); + } + this.docVersions = [...computedVersions, ...versions]; + + // Find the current version - eithers title matches the current deployment mode + // or its title matches the major version of the current version info + this.currentDocVersion = this.docVersions.find(version => + version.title === this.deployment.mode || version.title === `v${versionInfo.major}`); + this.currentDocVersion.title += ` (v${versionInfo.raw})`; }); this.navigationService.navigationViews.subscribe(views => { @@ -256,12 +275,13 @@ export class AppComponent implements OnInit { } updateHostClasses() { + const mode = `mode-${this.deployment.mode}`; const sideNavOpen = `sidenav-${this.sidenav.opened ? 'open' : 'closed'}`; const pageClass = `page-${this.pageId}`; const folderClass = `folder-${this.folderId}`; const viewClasses = Object.keys(this.currentNodes || {}).map(view => `view-${view}`).join(' '); - this.hostClasses = `${sideNavOpen} ${pageClass} ${folderClass} ${viewClasses}`; + this.hostClasses = `${mode} ${sideNavOpen} ${pageClass} ${folderClass} ${viewClasses}`; } // Dynamically change height of table of contents container diff --git a/aio/src/app/app.module.ts b/aio/src/app/app.module.ts index 73e0154342..ca9545ab5b 100644 --- a/aio/src/app/app.module.ts +++ b/aio/src/app/app.module.ts @@ -26,8 +26,10 @@ import { SwUpdatesModule } from 'app/sw-updates/sw-updates.module'; import { AppComponent } from 'app/app.component'; import { ApiService } from 'app/embedded/api/api.service'; import { CustomMdIconRegistry, SVG_ICONS } from 'app/shared/custom-md-icon-registry'; +import { Deployment } from 'app/shared/deployment.service'; import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component'; import { DtComponent } from 'app/layout/doc-viewer/dt.component'; +import { ModeBannerComponent } from 'app/layout/mode-banner/mode-banner.component'; import { EmbeddedModule } from 'app/embedded/embedded.module'; import { GaService } from 'app/shared/ga.service'; import { Logger } from 'app/shared/logger.service'; @@ -90,14 +92,16 @@ export const svgIconProviders = [ DocViewerComponent, DtComponent, FooterComponent, - TopMenuComponent, + ModeBannerComponent, NavMenuComponent, NavItemComponent, SearchResultsComponent, SearchBoxComponent, + TopMenuComponent, ], providers: [ ApiService, + Deployment, DocumentService, GaService, Logger, diff --git a/aio/src/app/layout/mode-banner/mode-banner.component.ts b/aio/src/app/layout/mode-banner/mode-banner.component.ts new file mode 100644 index 0000000000..366a0154e9 --- /dev/null +++ b/aio/src/app/layout/mode-banner/mode-banner.component.ts @@ -0,0 +1,16 @@ +import { Component, Input } from '@angular/core'; +import { VersionInfo } from 'app/navigation/navigation.service'; + +@Component({ + selector: 'aio-mode-banner', + template: ` +
+ This is the archived documentation for Angular v{{version?.major}}. + Please visit angular.io to see documentation for the current version of Angular. +
+ ` +}) +export class ModeBannerComponent { + @Input() mode: string; + @Input() version: VersionInfo; +} diff --git a/aio/src/app/shared/deployment.service.spec.ts b/aio/src/app/shared/deployment.service.spec.ts new file mode 100644 index 0000000000..34df4ec92e --- /dev/null +++ b/aio/src/app/shared/deployment.service.spec.ts @@ -0,0 +1,32 @@ +import { ReflectiveInjector } from '@angular/core'; +import { environment } from 'environments/environment'; +import { LocationService } from 'app/shared/location.service'; +import { MockLocationService } from 'testing/location.service'; +import { Deployment } from './deployment.service'; + +describe('Deployment service', () => { + describe('mode', () => { + it('should get the mode from the environment', () => { + environment.mode = 'foo'; + const deployment = getInjector().get(Deployment); + expect(deployment.mode).toEqual('foo'); + }); + + it('should get the mode from the `mode` query parameter if available', () => { + const injector = getInjector(); + + const locationService: MockLocationService = injector.get(LocationService); + locationService.search.and.returnValue({ mode: 'bar' }); + + const deployment = injector.get(Deployment); + expect(deployment.mode).toEqual('bar'); + }); + }); +}); + +function getInjector() { + return ReflectiveInjector.resolveAndCreate([ + Deployment, + { provide: LocationService, useFactory: () => new MockLocationService('') } + ]); +} diff --git a/aio/src/app/shared/deployment.service.ts b/aio/src/app/shared/deployment.service.ts new file mode 100644 index 0000000000..6a3f0ee79a --- /dev/null +++ b/aio/src/app/shared/deployment.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { LocationService } from 'app/shared/location.service'; +import { environment } from 'environments/environment'; + +/** + * Information about the deployment of this application. + */ +@Injectable() +export class Deployment { + /** + * The deployment mode set from the environment provided at build time; + * or overridden by the `mode` query parameter: e.g. `...?mode=archive` + */ + mode: string = this.location.search()['mode'] || environment.mode; + + constructor(private location: LocationService) {} +}; diff --git a/aio/src/app/shared/location.service.ts b/aio/src/app/shared/location.service.ts index 00d042ad70..52ecacfa5c 100644 --- a/aio/src/app/shared/location.service.ts +++ b/aio/src/app/shared/location.service.ts @@ -55,6 +55,10 @@ export class LocationService { window.location.assign(url); } + replace(url: string) { + window.location.replace(url); + } + private stripSlashes(url: string) { return url.replace(/^\/+/, '').replace(/\/+(\?|#|$)/, '$1'); } diff --git a/aio/src/environments/environment.archive.ts b/aio/src/environments/environment.archive.ts new file mode 100644 index 0000000000..5100c50a77 --- /dev/null +++ b/aio/src/environments/environment.archive.ts @@ -0,0 +1,6 @@ +// This is for archived sites, which are hosted at https://vX.angular.io, where X is the major Angular version. +export const environment = { + gaId: 'UA-8594346-15', // Production id (since it is linked from the main site) + production: true, + mode: 'archive' +}; diff --git a/aio/src/environments/environment.next.ts b/aio/src/environments/environment.next.ts new file mode 100644 index 0000000000..63682ea65e --- /dev/null +++ b/aio/src/environments/environment.next.ts @@ -0,0 +1,6 @@ +// This is for the staging site, which is hosted at https://next.angular.io (and https://aio-staging.firebaseapp.org) +export const environment = { + gaId: 'UA-8594346-15', // Production id (since it is linked from the main site) + production: true, + mode: 'next' +}; diff --git a/aio/src/environments/environment.prod.ts b/aio/src/environments/environment.stable.ts similarity index 57% rename from aio/src/environments/environment.prod.ts rename to aio/src/environments/environment.stable.ts index 94acf34f85..47e330840b 100644 --- a/aio/src/environments/environment.prod.ts +++ b/aio/src/environments/environment.stable.ts @@ -1,5 +1,6 @@ // This is for the production site, which is hosted at https://angular.io export const environment = { - gaId: 'UA-80456300-1', - production: true + gaId: 'UA-80456300-1', // Production id + production: true, + mode: 'stable' }; diff --git a/aio/src/environments/environment.stage.ts b/aio/src/environments/environment.stage.ts deleted file mode 100644 index 97a992c88f..0000000000 --- a/aio/src/environments/environment.stage.ts +++ /dev/null @@ -1,5 +0,0 @@ -// This is for the staging site, which is hosted at https://aio-staging.firebaseapp.org -export const environment = { - gaId: 'UA-8594346-26', - production: true -}; diff --git a/aio/src/environments/environment.ts b/aio/src/environments/environment.ts index 165a718f55..77d62f0968 100644 --- a/aio/src/environments/environment.ts +++ b/aio/src/environments/environment.ts @@ -13,6 +13,7 @@ import 'core-js/es7/reflect'; export const environment = { - gaId: 'UA-8594346-26', // Staging site - production: false + gaId: 'UA-8594346-26', // Development id + production: false, + mode: 'stable' }; diff --git a/aio/src/extra-files/README.md b/aio/src/extra-files/README.md new file mode 100644 index 0000000000..2aad208a30 --- /dev/null +++ b/aio/src/extra-files/README.md @@ -0,0 +1,9 @@ +# Extra files folder + +This folder is used for extra files that should be included in deployments to firebase. + +After the AIO application had been built and before it is deployed all files and folders +inside the folder with the same name as the current deployment mode (next, stable, archive) +will be copied to the `dist` folder. + +See the `scripts/deploy-to-firebase.sh` script for more detail. \ No newline at end of file diff --git a/aio/src/extra-files/archive/robots.txt b/aio/src/extra-files/archive/robots.txt new file mode 100644 index 0000000000..77470cb39f --- /dev/null +++ b/aio/src/extra-files/archive/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/aio/src/extra-files/next/robots.txt b/aio/src/extra-files/next/robots.txt new file mode 100644 index 0000000000..77470cb39f --- /dev/null +++ b/aio/src/extra-files/next/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/aio/src/styles/2-modules/_deploy-theme.scss b/aio/src/styles/2-modules/_deploy-theme.scss new file mode 100644 index 0000000000..3858b66ce7 --- /dev/null +++ b/aio/src/styles/2-modules/_deploy-theme.scss @@ -0,0 +1,50 @@ + +aio-shell.mode-archive { + + .mat-toolbar.mat-primary, footer { + background: linear-gradient(145deg,#263238,#78909C); + } + + .vertical-menu-item { + &.selected, &:hover { + color: #263238; + } + } + + .toc-inner ul.toc-list li.active a { + color: #263238; + + &:before { + background-color: #263238; + } + } + + .toc-inner ul.toc-list li:hover a { + color: #263238; + } +} + +aio-shell.mode-next { + + .mat-toolbar.mat-primary, footer { + background: linear-gradient(145deg,#DD0031,#C3002F); + } + + .vertical-menu-item { + &.selected, &:hover { + color: #DD0031; + } + } + + .toc-inner ul.toc-list li.active a { + color: #DD0031; + + &:before { + background-color: #DD0031; + } + } + + .toc-inner ul.toc-list li:hover a { + color: #DD0031; + } +} diff --git a/aio/src/styles/2-modules/_modules-dir.scss b/aio/src/styles/2-modules/_modules-dir.scss index c145331196..4828b05e68 100644 --- a/aio/src/styles/2-modules/_modules-dir.scss +++ b/aio/src/styles/2-modules/_modules-dir.scss @@ -29,3 +29,4 @@ @import 'subsection'; @import 'toc'; @import 'select-menu'; + @import 'deploy-theme'; diff --git a/aio/src/testing/location.service.ts b/aio/src/testing/location.service.ts index 2656769b95..1e03a69626 100644 --- a/aio/src/testing/location.service.ts +++ b/aio/src/testing/location.service.ts @@ -10,6 +10,7 @@ export class MockLocationService { go = jasmine.createSpy('Location.go').and .callFake((url: string) => this.urlSubject.next(url)); goExternal = jasmine.createSpy('Location.goExternal'); + replace = jasmine.createSpy('Location.replace'); handleAnchorClick = jasmine.createSpy('Location.handleAnchorClick') .and.returnValue(false); // prevent click from causing a browser navigation diff --git a/aio/tools/cli-patches/ngo.patch b/aio/tools/cli-patches/ngo.patch deleted file mode 100644 index 44c74b987f..0000000000 --- a/aio/tools/cli-patches/ngo.patch +++ /dev/null @@ -1,14 +0,0 @@ ---- node_modules/@angular/cli/models/webpack-configs/production.js 2017-05-12 14:30:22.000000000 -0700 -+++ node_modules/@angular/cli/models/webpack-configs/production.js 2017-05-12 14:32:23.000000000 -0700 -@@ -68,6 +68,11 @@ - } - return { - entry: entryPoints, -+ module: { -+ rules: [ -+ {"test": /\.js$/, "use": {loader: "ngo/webpack-loader", options: { sourceMap: true }}}, -+ ] -+ }, - plugins: [ - new webpack.EnvironmentPlugin({ - 'NODE_ENV': 'production' diff --git a/aio/tools/cli-patches/patch.js b/aio/tools/cli-patches/patch.js deleted file mode 100644 index b5735eee16..0000000000 --- a/aio/tools/cli-patches/patch.js +++ /dev/null @@ -1,12 +0,0 @@ -const fs = require('fs'); -const sh = require('shelljs'); - -PATCH_LOCK = 'node_modules/@angular/cli/models/webpack-configs/.patched'; - -if (!fs.existsSync(PATCH_LOCK)) { - sh.exec('patch -p0 -i tools/cli-patches/ngo.patch'); - sh.exec('patch -p0 -i tools/cli-patches/purify.patch'); - sh.exec('patch -p0 -i tools/cli-patches/scope-hoisting.patch'); - sh.exec('patch -p0 -i tools/cli-patches/uglify-config.patch'); - sh.touch(PATCH_LOCK); -} diff --git a/aio/tools/cli-patches/purify.patch b/aio/tools/cli-patches/purify.patch deleted file mode 100644 index fbe8484169..0000000000 --- a/aio/tools/cli-patches/purify.patch +++ /dev/null @@ -1,10 +0,0 @@ ---- node_modules/@angular/cli/models/webpack-configs/production.js 2017-05-11 12:10:46.000000000 -0700 -+++ node_modules/@angular/cli/models/webpack-configs/production.js 2017-05-11 12:10:11.000000000 -0700 -@@ -73,6 +73,7 @@ - 'NODE_ENV': 'production' - }), - new webpack.HashedModuleIdsPlugin(), -+ new (require("ngo").PurifyPlugin)(), - new webpack.optimize.UglifyJsPlugin({ - mangle: { screw_ie8: true }, - compress: { screw_ie8: true, warnings: buildOptions.verbose }, diff --git a/aio/tools/cli-patches/scope-hoisting.patch b/aio/tools/cli-patches/scope-hoisting.patch deleted file mode 100644 index 15c05ce456..0000000000 --- a/aio/tools/cli-patches/scope-hoisting.patch +++ /dev/null @@ -1,10 +0,0 @@ ---- node_modules/@angular/cli/models/webpack-configs/production.js 2017-05-24 15:36:43.000000000 -0700 -+++ node_modules/@angular/cli/models/webpack-configs/production.js 2017-05-24 15:37:04.000000000 -0700 -@@ -85,6 +85,7 @@ - 'NODE_ENV': 'production' - }), - new webpack.HashedModuleIdsPlugin(), -+ new webpack.optimize.ModuleConcatenationPlugin(), - new (require("ngo").PurifyPlugin)(), - new webpack.optimize.UglifyJsPlugin({ - mangle: true, diff --git a/aio/tools/cli-patches/uglify-config.patch b/aio/tools/cli-patches/uglify-config.patch deleted file mode 100644 index 3d17d3c9d4..0000000000 --- a/aio/tools/cli-patches/uglify-config.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- node_modules/@angular/cli/models/webpack-configs/production.js 2017-05-24 15:36:43.000000000 -0700 -+++ node_modules/@angular/cli/models/webpack-configs/production.js 2017-05-24 15:37:04.000000000 -0700 -@@ -82,7 +82,7 @@ - new (require("purify/purify-webpack-plugin"))(), - new webpack.optimize.UglifyJsPlugin({ - mangle: { screw_ie8: true }, -- compress: { screw_ie8: true, warnings: buildOptions.verbose }, -+ compress: { screw_ie8: true, warnings: buildOptions.verbose, pure_getters: true }, - sourceMap: buildOptions.sourcemaps, - comments: false - }) diff --git a/aio/tools/transforms/angular-base-package/post-processors/h1-checker.js b/aio/tools/transforms/angular-base-package/post-processors/h1-checker.js index ee5356b5e9..e43517e571 100644 --- a/aio/tools/transforms/angular-base-package/post-processors/h1-checker.js +++ b/aio/tools/transforms/angular-base-package/post-processors/h1-checker.js @@ -37,5 +37,5 @@ function getText(h1) { (node.properties.ariaHidden === 'true' || node.properties['aria-hidden'] === 'true') )); - return toString(cleaned); -} \ No newline at end of file + return cleaned ? toString(cleaned) : ''; +} diff --git a/aio/tools/transforms/angular-base-package/post-processors/h1-checker.spec.js b/aio/tools/transforms/angular-base-package/post-processors/h1-checker.spec.js index 00ee5490ff..4dd2537df4 100644 --- a/aio/tools/transforms/angular-base-package/post-processors/h1-checker.spec.js +++ b/aio/tools/transforms/angular-base-package/post-processors/h1-checker.spec.js @@ -70,4 +70,14 @@ describe('h1Checker postprocessor', () => { processor.$process([doc]); expect(doc.vFile.title).toEqual('What is Angular?'); }); -}); \ No newline at end of file + + it('should not break if the h1 is empty (except for an aria-hidden anchor)', () => { + const doc = { + docType: 'a', + renderedContent: ` +

+ ` + }; + expect(() => processor.$process([doc])).not.toThrow(); + }); +}); diff --git a/aio/yarn.lock b/aio/yarn.lock index c7be4d5000..9e32cc0b32 100644 --- a/aio/yarn.lock +++ b/aio/yarn.lock @@ -2,6 +2,15 @@ # yarn lockfile v1 +"@angular-devkit/build-optimizer@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.0.3.tgz#092bdf732b79a779ce540f9bb99d6590dd971204" + dependencies: + loader-utils "^1.1.0" + magic-string "^0.19.1" + source-map "^0.5.6" + typescript "^2.3.3" + "@angular/animations@^4.3.1": version "4.3.1" resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-4.3.1.tgz#1f7e0bb803efc21c608246e6765a1c647f3d1a5f" @@ -14,51 +23,54 @@ dependencies: tslib "^1.7.1" -"@angular/cli@angular/cli-builds#webpack-next": - version "1.2.0-beta.0-7c33dd4" - resolved "https://codeload.github.com/angular/cli-builds/tar.gz/b4bb5968c04c92fd816c434044e28bbfc60b296c" +"@angular/cli@1.3.0-rc.3": + version "1.3.0-rc.3" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-1.3.0-rc.3.tgz#5a6999382f956b6109d3042569659972bca38a63" dependencies: + "@angular-devkit/build-optimizer" "0.0.3" "@ngtools/json-schema" "1.1.0" - "@ngtools/webpack" "https://github.com/angular/ngtools-webpack-builds.git#7c33dd4" + "@ngtools/webpack" "1.6.0-rc.3" autoprefixer "^6.5.3" - chalk "^1.1.3" + chalk "^2.0.1" + circular-dependency-plugin "^3.0.0" common-tags "^1.3.1" + core-object "^3.1.0" css-loader "^0.28.1" cssnano "^3.10.0" - debug "^2.1.3" denodeify "^1.2.1" diff "^3.1.0" ember-cli-normalize-entity-name "^1.0.0" ember-cli-string-utils "^1.0.0" exports-loader "^0.6.3" - extract-text-webpack-plugin "^2.1.0" + extract-text-webpack-plugin "3.0.0" file-loader "^0.10.0" - fs-extra "^2.0.0" + fs-extra "^4.0.0" get-caller-file "^1.0.0" glob "^7.0.3" - html-webpack-plugin "^2.19.0" + heimdalljs "^0.2.4" + heimdalljs-logger "^0.1.9" + html-webpack-plugin "^2.29.0" inflection "^1.7.0" inquirer "^3.0.0" isbinaryfile "^3.0.0" istanbul-instrumenter-loader "^2.0.0" - json-loader "^0.5.4" + karma-source-map-support "^1.2.0" less "^2.7.2" - less-loader "^4.0.2" + less-loader "^4.0.5" license-webpack-plugin "^0.4.2" lodash "^4.11.1" memory-fs "^0.4.1" minimatch "^3.0.3" node-modules-path "^1.0.0" nopt "^4.0.1" - opn "4.0.2" + opn "~5.1.0" portfinder "~1.0.12" postcss-loader "^1.3.3" postcss-url "^5.1.2" raw-loader "^0.5.1" resolve "^1.1.7" - rimraf "^2.5.3" rsvp "^3.0.17" - rxjs "^5.0.1" + rxjs "^5.4.2" sass-loader "^6.0.3" script-loader "^0.7.0" semver "^5.1.0" @@ -68,14 +80,14 @@ stylus "^0.54.5" stylus-loader "^3.0.1" temp "0.8.3" - typescript ">=2.0.0 <2.4.0" + typescript ">=2.0.0 <2.5.0" url-loader "^0.5.7" walk-sync "^0.3.1" - webpack "3.0.0-rc.1" - webpack-dev-middleware "^1.10.2" - webpack-dev-server "~2.4.5" - webpack-merge "^2.4.0" - zone.js "^0.8.4" + webpack "~3.4.1" + webpack-dev-middleware "^1.11.0" + webpack-dev-server "~2.5.1" + webpack-merge "^4.1.0" + zone.js "^0.8.14" optionalDependencies: node-sass "^4.3.0" @@ -166,13 +178,12 @@ version "1.1.0" resolved "https://registry.yarnpkg.com/@ngtools/json-schema/-/json-schema-1.1.0.tgz#c3a0c544d62392acc2813a42c8a0dc6f58f86922" -"@ngtools/webpack@https://github.com/angular/ngtools-webpack-builds.git#7c33dd4": - version "1.4.0-7c33dd4" - resolved "https://github.com/angular/ngtools-webpack-builds.git#7c33dd4" +"@ngtools/webpack@1.6.0-rc.3": + version "1.6.0-rc.3" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-1.6.0-rc.3.tgz#1cc0885c5075f66ac322e68eaf256bff172dd134" dependencies: - enhanced-resolve "^3.1.0" loader-utils "^1.0.2" - magic-string "^0.19.0" + magic-string "^0.22.3" source-map "^0.5.6" "@types/jasmine@^2.5.52": @@ -280,14 +291,14 @@ ajv-keywords@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0" -ajv@^4.11.2, ajv@^4.7.0, ajv@^4.9.1: +ajv@^4.7.0, ajv@^4.9.1: version "4.11.8" resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" dependencies: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^5.1.5: +ajv@^5.0.0, ajv@^5.1.5: version "5.1.5" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.1.5.tgz#8734931b601f00d4feef7c65738d77d1b65d1f68" dependencies: @@ -353,6 +364,12 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" +ansi-styles@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + any-promise@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -451,7 +468,7 @@ array-flatten@1.1.1, array-flatten@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" -array-flatten@2.1.1: +array-flatten@2.1.1, array-flatten@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296" @@ -549,12 +566,18 @@ async@^1.3.0, async@^1.4.0, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@^2.0.0, async@^2.1.2, async@^2.1.4, async@^2.1.5: +async@^2.0.0: version "2.4.0" resolved "https://registry.yarnpkg.com/async/-/async-2.4.0.tgz#4990200f18ea5b837c2cc4f8c031a6985c385611" dependencies: lodash "^4.14.0" +async@^2.1.2, async@^2.1.4, async@^2.1.5, async@^2.4.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" + dependencies: + lodash "^4.14.0" + async@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" @@ -772,6 +795,17 @@ body-parser@^1.16.1: raw-body "~2.2.0" type-is "~1.6.15" +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + boolbase@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -899,6 +933,10 @@ buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" +buffer-indexof@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.0.tgz#f54f647c4f4e25228baa656a2e57e43d5f270982" + buffer-shims@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" @@ -971,7 +1009,7 @@ camelcase@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" -camelcase@^4.0.0: +camelcase@^4.0.0, camelcase@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" @@ -1037,6 +1075,14 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +chalk@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.0.1.tgz#dbec49436d2ae15f536114e76d14656cdbc0f44d" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + change-case@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/change-case/-/change-case-3.0.0.tgz#6c9c8e35f8790870a82b6b0745be8c3cbef9b081" @@ -1084,7 +1130,7 @@ character-reference-invalid@^1.0.0: version "0.0.2" resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" -chokidar@^1.4.1, chokidar@^1.4.3, chokidar@^1.6.0: +chokidar@^1.4.1, chokidar@^1.6.0, chokidar@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" dependencies: @@ -1113,6 +1159,10 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: dependencies: inherits "^2.0.1" +circular-dependency-plugin@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/circular-dependency-plugin/-/circular-dependency-plugin-3.0.0.tgz#9b68692e35b0e3510998d0164b6ae5011bea5760" + circular-json@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" @@ -1236,7 +1286,7 @@ collapse-white-space@^1.0.0, collapse-white-space@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.2.tgz#9c463fb9c6d190d2dcae21a356a01bcae9eeef6d" -color-convert@^1.3.0: +color-convert@^1.3.0, color-convert@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" dependencies: @@ -1502,6 +1552,12 @@ core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" +core-object@^3.1.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/core-object/-/core-object-3.1.3.tgz#df399b3311bdb0c909e8aae8929fc3c1c4b25880" + dependencies: + chalk "^1.1.3" + core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -1594,7 +1650,7 @@ cross-spawn@^4.0.0: lru-cache "^4.0.1" which "^1.2.9" -cross-spawn@^5.1.0: +cross-spawn@^5.0.1, cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" dependencies: @@ -1796,7 +1852,7 @@ date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" -debug@*, debug@2, debug@2.6.7, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.6.3: +debug@*, debug@2, debug@2.6.7, debug@^2.1.1, debug@^2.2.0, debug@^2.6.3: version "2.6.7" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" dependencies: @@ -1830,6 +1886,10 @@ decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +deep-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + deep-extend@~0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" @@ -1860,6 +1920,17 @@ del@^2.0.2, del@^2.2.0: pinkie-promise "^2.0.0" rimraf "^2.2.8" +del@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" + dependencies: + globby "^6.1.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + p-map "^1.1.1" + pify "^3.0.0" + rimraf "^2.2.8" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -1992,6 +2063,23 @@ directory-encoder@^0.7.2: handlebars "^1.3.0" img-stats "^0.5.2" +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + +dns-packet@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.1.1.tgz#2369d45038af045f3898e6fa56862aed3f40296c" + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + dependencies: + buffer-indexof "^1.0.0" + doctrine@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63" @@ -2221,14 +2309,14 @@ engine.io@1.8.3: engine.io-parser "1.3.2" ws "1.1.2" -enhanced-resolve@^3.0.0, enhanced-resolve@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.1.0.tgz#9f4b626f577245edcf4b2ad83d86e17f4f421dec" +enhanced-resolve@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" dependencies: graceful-fs "^4.1.2" memory-fs "^0.4.0" object-assign "^4.0.1" - tapable "^0.2.5" + tapable "^0.2.7" ensure-posix-path@^1.0.0: version "1.0.2" @@ -2468,6 +2556,18 @@ execa@^0.4.0: path-key "^1.0.0" strip-eof "^1.0.0" +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + exit-code@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/exit-code/-/exit-code-1.0.2.tgz#ce165811c9f117af6a5f882940b96ae7f9aecc34" @@ -2570,14 +2670,14 @@ extglob@^0.3.1: dependencies: is-extglob "^1.0.0" -extract-text-webpack-plugin@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-2.1.0.tgz#69315b885f876dbf96d3819f6a9f1cca7aebf159" +extract-text-webpack-plugin@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.0.tgz#90caa7907bc449f335005e3ac7532b41b00de612" dependencies: - ajv "^4.11.2" - async "^2.1.2" - loader-utils "^1.0.2" - webpack-sources "^0.1.0" + async "^2.4.1" + loader-utils "^1.1.0" + schema-utils "^0.3.0" + webpack-sources "^1.0.1" extsprintf@1.0.2: version "1.0.2" @@ -2687,6 +2787,12 @@ find-up@^1.0.0: path-exists "^2.0.0" pinkie-promise "^2.0.0" +find-up@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + findup-sync@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16" @@ -2824,13 +2930,21 @@ fs-extra@^0.30.0: path-is-absolute "^1.0.0" rimraf "^2.2.8" -fs-extra@^2.0.0, fs-extra@^2.1.2: +fs-extra@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-2.1.2.tgz#046c70163cef9aad46b0e4a7fa467fb22d71de35" dependencies: graceful-fs "^4.1.2" jsonfile "^2.1.0" +fs-extra@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.0.tgz#414fb4ca2d2170ba0014159d3a8aec3303418d9e" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^3.0.0" + universalify "^0.1.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -3128,6 +3242,10 @@ has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -3238,6 +3356,19 @@ header-case@^1.0.0: no-case "^2.2.0" upper-case "^1.1.3" +heimdalljs-logger@^0.1.9: + version "0.1.9" + resolved "https://registry.yarnpkg.com/heimdalljs-logger/-/heimdalljs-logger-0.1.9.tgz#d76ada4e45b7bb6f786fc9c010a68eb2e2faf176" + dependencies: + debug "^2.2.0" + heimdalljs "^0.2.0" + +heimdalljs@^0.2.0, heimdalljs@^0.2.4: + version "0.2.5" + resolved "https://registry.yarnpkg.com/heimdalljs/-/heimdalljs-0.2.5.tgz#6aa54308eee793b642cff9cf94781445f37730ac" + dependencies: + rsvp "~3.2.1" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -3298,9 +3429,9 @@ html-void-elements@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.1.tgz#f929bea267a19e3535950502ca12c159f1b559af" -html-webpack-plugin@^2.19.0: - version "2.28.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-2.28.0.tgz#2e7863b57e5fd48fe263303e2ffc934c3064d009" +html-webpack-plugin@^2.29.0: + version "2.29.0" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-2.29.0.tgz#e987f421853d3b6938c8c4c8171842e5fd17af23" dependencies: bluebird "^3.4.7" html-minifier "^3.2.3" @@ -3535,6 +3666,12 @@ inquirer@^3.0.0: strip-ansi "^3.0.0" through "^2.3.6" +internal-ip@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c" + dependencies: + meow "^3.3.0" + interpret@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90" @@ -3549,6 +3686,10 @@ invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" +ip@^1.1.0: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + ipaddr.js@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec" @@ -3779,6 +3920,10 @@ is-word-character@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.0.tgz#a3a9e5ddad70c5c2ee36f4a9cfc9a53f44535247" +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -4025,6 +4170,12 @@ jsonfile@^2.1.0: optionalDependencies: graceful-fs "^4.1.6" +jsonfile@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66" + optionalDependencies: + graceful-fs "^4.1.6" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -4105,6 +4256,12 @@ karma-jasmine@^1.0.2, karma-jasmine@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-1.1.0.tgz#22e4c06bf9a182e5294d1f705e3733811b810acf" +karma-source-map-support@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/karma-source-map-support/-/karma-source-map-support-1.2.0.tgz#1bf81e7bb4b089627ab352ec4179e117c406a540" + dependencies: + source-map-support "^0.4.1" + karma@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/karma/-/karma-1.7.0.tgz#6f7a1a406446fa2e187ec95398698f4cee476269" @@ -4211,9 +4368,9 @@ lcid@^1.0.0: dependencies: invert-kv "^1.0.0" -less-loader@^4.0.2: - version "4.0.3" - resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-4.0.3.tgz#d1e6462ca2f090c11248455e14b8dda4616d0521" +less-loader@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-4.0.5.tgz#ae155a7406cac6acd293d785587fcff0f478c4dd" dependencies: clone "^2.1.1" loader-utils "^1.1.0" @@ -4279,6 +4436,15 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + loader-runner@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" @@ -4300,6 +4466,13 @@ loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: emojis-list "^2.0.0" json5 "^0.5.0" +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + lodash._isnative@~2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/lodash._isnative/-/lodash._isnative-2.4.1.tgz#3ea6404b784a7be836c7b57580e1cdf79b14832c" @@ -4451,12 +4624,18 @@ macaddress@^0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" -magic-string@^0.19.0, magic-string@^0.19.1: +magic-string@^0.19.1: version "0.19.1" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.19.1.tgz#14d768013caf2ec8fdea16a49af82fc377e75201" dependencies: vlq "^0.2.1" +magic-string@^0.22.3: + version "0.22.4" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.4.tgz#31039b4e40366395618c1d6cf8193c53917475ff" + dependencies: + vlq "^0.2.1" + make-dir@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978" @@ -4528,6 +4707,12 @@ media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" @@ -4535,7 +4720,7 @@ memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: errno "^0.1.3" readable-stream "^2.0.1" -meow@^3.7.0: +meow@^3.3.0, meow@^3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" dependencies: @@ -4680,6 +4865,17 @@ ms@^0.7.1: version "0.7.3" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.3.tgz#708155a5e44e33f5fd0fc53e81d0d40a91be1fff" +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + +multicast-dns@^6.0.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.1.1.tgz#6e7de86a570872ab17058adea7160bbeca814dde" + dependencies: + dns-packet "^1.0.1" + thunky "^0.1.0" + mute-stream@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" @@ -4730,21 +4926,16 @@ ng-pwa-tools@^0.0.10: sha1 "^1.1.1" ts-node "^3.0.2" -ngo@angular/ngo: - version "0.0.11" - resolved "https://codeload.github.com/angular/ngo/tar.gz/09980bf1006a20963a7273467c20d28216035d16" - dependencies: - loader-utils "^1.1.0" - magic-string "^0.19.1" - source-map "^0.5.6" - typescript "^2.3.3" - no-case@^2.2.0: version "2.3.1" resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.1.tgz#7aeba1c73a52184265554b7dc03baf720df80081" dependencies: lower-case "^1.1.1" +node-forge@0.6.33: + version "0.6.33" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc" + node-gyp@^3.3.1: version "3.6.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.1.tgz#19561067ff185464aded478212681f47fd578cbc" @@ -4899,6 +5090,12 @@ npm-run-path@^1.0.0: dependencies: path-key "^1.0.0" +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + "npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.0.tgz#dc59bee85f64f00ed424efb2af0783df25d1c0b5" @@ -5022,6 +5219,12 @@ opn@4.0.2: object-assign "^4.0.1" pinkie-promise "^2.0.0" +opn@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.1.0.tgz#72ce2306a17dbea58ff1041853352b4a8fc77519" + dependencies: + is-wsl "^1.1.0" + optimist@0.6.x, optimist@^0.6.1, optimist@~0.6.0, optimist@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" @@ -5079,6 +5282,14 @@ os-locale@^1.4.0: dependencies: lcid "^1.0.0" +os-locale@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + os-tmpdir@^1.0.0, os-tmpdir@~1.0.0, os-tmpdir@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -5090,6 +5301,24 @@ osenv@0, osenv@^0.1.0, osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + +p-limit@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-map@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a" + package-json@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/package-json/-/package-json-1.2.0.tgz#c8ecac094227cdf76a316874ed05e27cc939a0e0" @@ -5221,6 +5450,10 @@ path-exists@^2.0.0: dependencies: pinkie-promise "^2.0.0" +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -5233,6 +5466,10 @@ path-key@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/path-key/-/path-key-1.0.0.tgz#5d53d578019646c0d68800db4e146e6bdc2ac7af" +path-key@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + path-parse@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" @@ -5255,6 +5492,12 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + pbkdf2@^3.0.3: version "3.0.12" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.12.tgz#be36785c5067ea48d806ff923288c5f750b6b8a2" @@ -5273,6 +5516,10 @@ pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" @@ -5775,6 +6022,13 @@ read-pkg-up@^1.0.1: find-up "^1.0.0" read-pkg "^1.0.0" +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + read-pkg@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" @@ -5783,6 +6037,14 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + readable-stream@1.0, readable-stream@~1.0.2, readable-stream@~1.0.24, readable-stream@~1.0.26: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" @@ -6138,7 +6400,7 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.3, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1: +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" dependencies: @@ -6171,6 +6433,10 @@ rsvp@^3.0.17, rsvp@^3.0.18, rsvp@^3.1.0: version "3.5.0" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.5.0.tgz#a62c573a4ae4e1dfd0697ebc6242e79c681eaa34" +rsvp@~3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.2.1.tgz#07cb4a5df25add9e826ebc67dcc9fd89db27d84a" + run-async@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" @@ -6195,12 +6461,18 @@ rx@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" -rxjs@^5.0.1, rxjs@^5.2.0: +rxjs@^5.2.0: version "5.4.0" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.0.tgz#a7db14ab157f9d7aac6a56e655e7a3860d39bf26" dependencies: symbol-observable "^1.0.1" +rxjs@^5.4.2: + version "5.4.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.2.tgz#2a3236fcbf03df57bae06fd6972fd99e5c08fcf7" + dependencies: + symbol-observable "^1.0.1" + safe-buffer@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" @@ -6263,6 +6535,12 @@ scandirectory@^2.5.0: safefs "^3.1.2" taskgroup "^4.0.5" +schema-utils@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" + dependencies: + ajv "^5.0.0" + script-loader@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/script-loader/-/script-loader-0.7.0.tgz#685dc7e7069e0dee7a92674f0ebc5b0f55baa5ec" @@ -6299,6 +6577,12 @@ selenium-webdriver@^2.53.2: ws "^1.0.1" xml2js "0.4.4" +selfsigned@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.9.1.tgz#cdda4492d70d486570f87c65546023558e1dfa5a" + dependencies: + node-forge "0.6.33" + semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" @@ -6527,7 +6811,7 @@ sort-keys@^1.0.0: dependencies: is-plain-obj "^1.0.0" -source-list-map@^0.1.7, source-list-map@~0.1.7: +source-list-map@^0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106" @@ -6543,7 +6827,7 @@ source-map-loader@^0.2.0: loader-utils "~0.2.2" source-map "~0.1.33" -source-map-support@^0.4.0, source-map-support@^0.4.15, source-map-support@^0.4.2, source-map-support@~0.4.0: +source-map-support@^0.4.0, source-map-support@^0.4.1, source-map-support@^0.4.15, source-map-support@^0.4.2, source-map-support@~0.4.0: version "0.4.15" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.15.tgz#03202df65c06d2bd8c7ec2362a193056fef8d3b1" dependencies: @@ -6848,12 +7132,18 @@ supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" -supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.1.2, supports-color@^3.2.3: +supports-color@^3.1.1, supports-color@^3.1.2, supports-color@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" dependencies: has-flag "^1.0.0" +supports-color@^4.0.0, supports-color@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.1.tgz#65a4bb2631e90e02420dba5554c375a4754bb836" + dependencies: + has-flag "^2.0.0" + svgo@^0.7.0: version "0.7.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" @@ -6892,9 +7182,9 @@ table@^3.7.8: slice-ansi "0.0.4" string-width "^2.0.0" -tapable@^0.2.5, tapable@~0.2.5: - version "0.2.6" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.6.tgz#206be8e188860b514425375e6f1ae89bfb01fd8d" +tapable@^0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.7.tgz#e46c0daacbb2b8a98b9b0cea0f4052105817ed5c" tar-pack@^3.4.0: version "3.4.0" @@ -6979,6 +7269,10 @@ through2@2.0.1: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" +thunky@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e" + timed-out@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-2.0.0.tgz#f38b0ae81d3747d628001f41dafc652ace671c0a" @@ -7210,7 +7504,7 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -typescript@2.3.2, "typescript@>=2.0.0 <2.4.0": +typescript@2.3.2, "typescript@>=2.0.0 <2.5.0": version "2.3.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.3.2.tgz#f0f045e196f69a72f06b25fd3bd39d01c3ce9984" @@ -7218,7 +7512,7 @@ typescript@^2.3.3, typescript@^2.3.4: version "2.4.1" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.1.tgz#c3ccb16ddaa0b2314de031e7e6fee89e5ba346bc" -uglify-js@^2.6, uglify-js@^2.8.27, uglify-js@~2.8.22: +uglify-js@^2.6, uglify-js@~2.8.22: version "2.8.28" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.28.tgz#e335032df9bb20dcb918f164589d5af47f38834a" dependencies: @@ -7227,6 +7521,15 @@ uglify-js@^2.6, uglify-js@^2.8.27, uglify-js@~2.8.22: optionalDependencies: uglify-to-browserify "~1.0.0" +uglify-js@^2.8.29: + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + uglify-js@^3.0.15: version "3.0.15" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.0.15.tgz#aacb323a846b234602270dead8a32441a8806f42" @@ -7246,6 +7549,14 @@ uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" +uglifyjs-webpack-plugin@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309" + dependencies: + source-map "^0.5.6" + uglify-js "^2.8.29" + webpack-sources "^1.0.1" + uid-number@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" @@ -7380,6 +7691,10 @@ universal-analytics@^0.3.9: request "2.x" underscore "1.x" +universalify@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -7609,12 +7924,12 @@ walkdir@^0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.0.11.tgz#a16d025eb931bd03b52f308caed0f40fcebe9532" -watchpack@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.3.1.tgz#7d8693907b28ce6013e7f3610aa2a1acf07dad87" +watchpack@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac" dependencies: async "^2.1.2" - chokidar "^1.4.3" + chokidar "^1.7.0" graceful-fs "^4.1.2" watchr@^3.0.1: @@ -7670,50 +7985,47 @@ webidl-conversions@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.1.tgz#8015a17ab83e7e1b311638486ace81da6ce206a0" -webpack-dev-middleware@^1.10.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.10.2.tgz#2e252ce1dfb020dbda1ccb37df26f30ab014dbd1" +webpack-dev-middleware@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.11.0.tgz#09691d0973a30ad1f82ac73a12e2087f0a4754f9" dependencies: memory-fs "~0.4.1" mime "^1.3.4" path-is-absolute "^1.0.0" range-parser "^1.0.3" -webpack-dev-server@~2.4.5: - version "2.4.5" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.4.5.tgz#31384ce81136be1080b4b4cde0eb9b90e54ee6cf" +webpack-dev-server@~2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.5.1.tgz#a02e726a87bb603db5d71abb7d6d2649bf10c769" dependencies: ansi-html "0.0.7" + bonjour "^3.5.0" chokidar "^1.6.0" compression "^1.5.2" connect-history-api-fallback "^1.3.0" + del "^3.0.0" express "^4.13.3" html-entities "^1.2.0" http-proxy-middleware "~0.17.4" + internal-ip "^1.2.0" opn "4.0.2" portfinder "^1.0.9" + selfsigned "^1.9.1" serve-index "^1.7.2" sockjs "0.3.18" sockjs-client "1.1.2" spdy "^3.4.1" strip-ansi "^3.0.0" supports-color "^3.1.1" - webpack-dev-middleware "^1.10.2" + webpack-dev-middleware "^1.11.0" yargs "^6.0.0" -webpack-merge@^2.4.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-2.6.1.tgz#f1d801d2c5d39f83ffec9f119240b3e3be994a1c" +webpack-merge@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.0.tgz#6ad72223b3e0b837e531e4597c199f909361511e" dependencies: lodash "^4.17.4" -webpack-sources@^0.1.0: - version "0.1.5" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.1.5.tgz#aa1f3abf0f0d74db7111c40e500b84f966640750" - dependencies: - source-list-map "~0.1.7" - source-map "~0.5.3" - webpack-sources@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf" @@ -7721,16 +8033,16 @@ webpack-sources@^1.0.1: source-list-map "^2.0.0" source-map "~0.5.3" -webpack@3.0.0-rc.1: - version "3.0.0-rc.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.0.0-rc.1.tgz#41319cda8040f53177ce999a5d8a333e4f264d75" +webpack@~3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.4.1.tgz#4c3f4f3fb318155a4db0cb6a36ff05c5697418f4" dependencies: acorn "^5.0.0" acorn-dynamic-import "^2.0.0" ajv "^5.1.5" ajv-keywords "^2.0.0" async "^2.1.2" - enhanced-resolve "^3.0.0" + enhanced-resolve "^3.4.0" escope "^3.6.0" interpret "^1.0.0" json-loader "^0.5.4" @@ -7741,12 +8053,12 @@ webpack@3.0.0-rc.1: mkdirp "~0.5.0" node-libs-browser "^2.0.0" source-map "^0.5.3" - supports-color "^3.1.0" - tapable "~0.2.5" - uglify-js "^2.8.27" - watchpack "^1.3.1" + supports-color "^4.2.1" + tapable "^0.2.7" + uglifyjs-webpack-plugin "^0.4.6" + watchpack "^1.4.0" webpack-sources "^1.0.1" - yargs "^6.0.0" + yargs "^8.0.2" websocket-driver@>=0.5.1: version "0.6.5" @@ -7790,6 +8102,10 @@ which-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + which@1, which@^1.2.1, which@^1.2.8, which@^1.2.9: version "1.2.14" resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5" @@ -7984,6 +8300,12 @@ yargs-parser@^5.0.0: dependencies: camelcase "^3.0.0" +yargs-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" + dependencies: + camelcase "^4.1.0" + yargs@3.32.0, yargs@^3.32.0: version "3.32.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" @@ -8032,6 +8354,24 @@ yargs@^7.0.2: y18n "^3.2.1" yargs-parser "^5.0.0" +yargs@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" + dependencies: + camelcase "^4.1.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + read-pkg-up "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^7.0.0" + yargs@~3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" @@ -8068,6 +8408,10 @@ zip-stream@~0.6.0: lodash "~3.10.1" readable-stream "~1.0.26" -zone.js@^0.8.12, zone.js@^0.8.4: +zone.js@^0.8.12: version "0.8.12" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.12.tgz#86ff5053c98aec291a0bf4bbac501d694a05cfbb" + +zone.js@^0.8.14: + version "0.8.14" + resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.14.tgz#0c4db24b178232274ccb43f78c99db7f3642b6cf" diff --git a/docs/TRIAGE_AND_LABELS.md b/docs/TRIAGE_AND_LABELS.md index 86736ef745..50da21211b 100644 --- a/docs/TRIAGE_AND_LABELS.md +++ b/docs/TRIAGE_AND_LABELS.md @@ -50,7 +50,7 @@ What kind of problem is this? * `type: RFC / discussion / question` * `type: bug` -* `type: chore` +* `type: docs` * `type: feature` * `type: performance` * `type: refactor` @@ -108,16 +108,31 @@ closing or reviewing PRs is a top priority ahead of other ongoing work. Every triaged PR must have a `pr_action` label assigned to it and an assignee: -* `pr_action: review` - work is complete and comment is needed from the assignee. -* `pr_action: cleanup` - more work is needed from the current assignee. -* `pr_action: discuss` - discussion is needed, to be led by the current assignee. -* `pr_action: merge` - the PR should be merged. Add this to a PR when you would like to - trigger automatic merging following a successful build. This is described in [COMMITTER.md](COMMITTER.md). +* `PR action: review` - work is complete and comment is needed from the assignee. +* `PR action: cleanup` - more work is needed from the current assignee. +* `PR action: discuss` - discussion is needed, to be led by the current assignee. +* `PR action: merge` - the PR is ready to be merged by the caretaker. In addition, PRs can have the following states: -* `pr_state: WIP` - PR is experimental or rapidly changing. Not ready for review or triage. -* `pr_state: blocked` - PR is blocked on an issue or other PR. Not ready for review or triage. +* `PR state: WIP` - PR is experimental or rapidly changing. Not ready for review or triage. +* `PR state: blocked` - PR is blocked on an issue or other PR. Not ready for review or triage. + + +## PR Target + +In our git workflow, we merge changes either to the `master` branch, the most recent patch branch (e.g. `4.3.x`), or to both. + +The decision about the target must be done by the PR author and/or reviewer. This decision is then honored when the PR is being merged. + +To communicate the target we use the following labels: + +* `PR target: master-only` +* `PR target: patch-only` +* `PR target: master & patch` +* `PR target: TBD` - the target is yet to be determined + +If a PR is missing the "PR target" label, or if the label is set to "TBD" when the PR is sent to the caretaker, the caretaker should reject the PR and request the appropriate target label to be applied before the PR is merged. ## PR Approvals diff --git a/modules/e2e_util/perf_util.ts b/modules/e2e_util/perf_util.ts index f49dd92475..d5673ff587 100644 --- a/modules/e2e_util/perf_util.ts +++ b/modules/e2e_util/perf_util.ts @@ -11,7 +11,7 @@ const yargs = require('yargs'); const nodeUuid = require('node-uuid'); import * as fs from 'fs-extra'; -import {SeleniumWebDriverAdapter, Options, JsonFileReporter, Validator, RegressionSlopeValidator, ConsoleReporter, SizeValidator, MultiReporter, MultiMetric, Runner, Provider} from '@angular/benchpress'; +import {SeleniumWebDriverAdapter, Options, JsonFileReporter, Validator, RegressionSlopeValidator, ConsoleReporter, SizeValidator, MultiReporter, MultiMetric, Runner, StaticProvider} from '@angular/benchpress'; import {readCommandLine as readE2eCommandLine, openBrowser} from './e2e_util'; let cmdArgs: {'sample-size': number, 'force-gc': boolean, 'dryrun': boolean, 'bundles': boolean}; @@ -59,7 +59,7 @@ function createBenchpressRunner(): Runner { } const resultsFolder = './dist/benchmark_results'; fs.ensureDirSync(resultsFolder); - const providers: Provider[] = [ + const providers: StaticProvider[] = [ SeleniumWebDriverAdapter.PROTRACTOR_PROVIDERS, {provide: Options.FORCE_GC, useValue: cmdArgs['force-gc']}, {provide: Options.DEFAULT_DESCRIPTION, useValue: {'runId': runId}}, JsonFileReporter.PROVIDERS, diff --git a/modules/playground/README.md b/modules/playground/README.md index 0f8e32d516..213a5faafb 100644 --- a/modules/playground/README.md +++ b/modules/playground/README.md @@ -1,6 +1,8 @@ # How to run the examples locally +``` $ cp -r ./modules/playground ./dist/all/ $ ./node_modules/.bin/tsc -p modules --emitDecoratorMetadata -w $ gulp serve $ open http://localhost:8000/all/playground/src/hello_world/index.html?bundles=false +``` \ No newline at end of file diff --git a/package.json b/package.json index f5bbab4e8b..41e326ee97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-srcs", - "version": "5.0.0-beta.1", + "version": "5.0.0-beta.2", "private": true, "branchPattern": "2.0.*", "description": "Angular - a web framework for modern web apps", diff --git a/packages/animations/browser/src/dsl/animation_ast.ts b/packages/animations/browser/src/dsl/animation_ast.ts index 54e7ffec86..0a8b1309cd 100644 --- a/packages/animations/browser/src/dsl/animation_ast.ts +++ b/packages/animations/browser/src/dsl/animation_ast.ts @@ -82,6 +82,7 @@ export class AnimateAst extends Ast { export class StyleAst extends Ast { public isEmptyStep = false; + public containsDynamicStyles = false; constructor( public styles: (ɵStyleData|string)[], public easing: string|null, diff --git a/packages/animations/browser/src/dsl/animation_ast_builder.ts b/packages/animations/browser/src/dsl/animation_ast_builder.ts index d373ab145c..3fc8d50d65 100644 --- a/packages/animations/browser/src/dsl/animation_ast_builder.ts +++ b/packages/animations/browser/src/dsl/animation_ast_builder.ts @@ -8,7 +8,7 @@ import {AUTO_STYLE, AnimateTimings, AnimationAnimateChildMetadata, AnimationAnimateMetadata, AnimationAnimateRefMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationOptions, AnimationQueryMetadata, AnimationQueryOptions, AnimationReferenceMetadata, AnimationSequenceMetadata, AnimationStaggerMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, AnimationTriggerMetadata, style, ɵStyleData} from '@angular/animations'; import {getOrSetAsInMap} from '../render/shared'; -import {ENTER_SELECTOR, LEAVE_SELECTOR, NG_ANIMATING_SELECTOR, NG_TRIGGER_SELECTOR, copyObj, normalizeAnimationEntry, resolveTiming, validateStyleParams} from '../util'; +import {ENTER_SELECTOR, LEAVE_SELECTOR, NG_ANIMATING_SELECTOR, NG_TRIGGER_SELECTOR, SUBSTITUTION_EXPR_START, copyObj, extractStyleParams, iteratorToArray, normalizeAnimationEntry, resolveTiming, validateStyleParams} from '../util'; import {AnimateAst, AnimateChildAst, AnimateRefAst, Ast, DynamicTimingAst, GroupAst, KeyframesAst, QueryAst, ReferenceAst, SequenceAst, StaggerAst, StateAst, StyleAst, TimingAst, TransitionAst, TriggerAst} from './animation_ast'; import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor'; @@ -112,7 +112,35 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor { } visitState(metadata: AnimationStateMetadata, context: AnimationAstBuilderContext): StateAst { - return new StateAst(metadata.name, this.visitStyle(metadata.styles, context)); + const styleAst = this.visitStyle(metadata.styles, context); + const astParams = (metadata.options && metadata.options.params) || null; + if (styleAst.containsDynamicStyles) { + const missingSubs = new Set(); + const params = astParams || {}; + styleAst.styles.forEach(value => { + if (isObject(value)) { + const stylesObj = value as any; + Object.keys(stylesObj).forEach(prop => { + extractStyleParams(stylesObj[prop]).forEach(sub => { + if (!params.hasOwnProperty(sub)) { + missingSubs.add(sub); + } + }); + }); + } + }); + if (missingSubs.size) { + const missingSubsArr = iteratorToArray(missingSubs.values()); + context.errors.push( + `state("${metadata.name}", ...) must define default values for all the following style substitutions: ${missingSubsArr.join(', ')}`); + } + } + + const stateAst = new StateAst(metadata.name, styleAst); + if (astParams) { + stateAst.options = {params: astParams}; + } + return stateAst; } visitTransition(metadata: AnimationTransitionMetadata, context: AnimationAstBuilderContext): @@ -201,11 +229,12 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor { } else { styles.push(styleTuple as ɵStyleData); } - }) + }); } else { styles.push(metadata.styles); } + let containsDynamicStyles = false; let collectedEasing: string|null = null; styles.forEach(styleData => { if (isObject(styleData)) { @@ -215,9 +244,21 @@ export class AnimationAstBuilderVisitor implements AnimationDslVisitor { collectedEasing = easing as string; delete styleMap['easing']; } + if (!containsDynamicStyles) { + for (let prop in styleMap) { + const value = styleMap[prop]; + if (value.toString().indexOf(SUBSTITUTION_EXPR_START) >= 0) { + containsDynamicStyles = true; + break; + } + } + } } }); - return new StyleAst(styles, collectedEasing, metadata.offset); + + const ast = new StyleAst(styles, collectedEasing, metadata.offset); + ast.containsDynamicStyles = containsDynamicStyles; + return ast; } private _validateStyleAst(ast: StyleAst, context: AnimationAstBuilderContext): void { diff --git a/packages/animations/browser/src/dsl/animation_transition_factory.ts b/packages/animations/browser/src/dsl/animation_transition_factory.ts index 390f77d32f..4eea313a8b 100644 --- a/packages/animations/browser/src/dsl/animation_transition_factory.ts +++ b/packages/animations/browser/src/dsl/animation_transition_factory.ts @@ -9,38 +9,51 @@ import {AnimationOptions, ɵStyleData} from '@angular/animations'; import {AnimationDriver} from '../render/animation_driver'; import {getOrSetAsInMap} from '../render/shared'; -import {iteratorToArray, mergeAnimationOptions} from '../util'; +import {copyObj, interpolateParams, iteratorToArray, mergeAnimationOptions} from '../util'; -import {TransitionAst} from './animation_ast'; +import {StyleAst, TransitionAst} from './animation_ast'; import {buildAnimationTimelines} from './animation_timeline_builder'; import {TransitionMatcherFn} from './animation_transition_expr'; import {AnimationTransitionInstruction, createTransitionInstruction} from './animation_transition_instruction'; import {ElementInstructionMap} from './element_instruction_map'; +const EMPTY_OBJECT = {}; + export class AnimationTransitionFactory { constructor( private _triggerName: string, public ast: TransitionAst, - private _stateStyles: {[stateName: string]: ɵStyleData}) {} + private _stateStyles: {[stateName: string]: AnimationStateStyles}) {} match(currentState: any, nextState: any): boolean { return oneOrMoreTransitionsMatch(this.ast.matchers, currentState, nextState); } + buildStyles(stateName: string, params: {[key: string]: any}, errors: any[]) { + const backupStateStyler = this._stateStyles['*']; + const stateStyler = this._stateStyles[stateName]; + const backupStyles = backupStateStyler ? backupStateStyler.buildStyles(params, errors) : {}; + return stateStyler ? stateStyler.buildStyles(params, errors) : backupStyles; + } + build( driver: AnimationDriver, element: any, currentState: any, nextState: any, - options?: AnimationOptions, + currentOptions?: AnimationOptions, nextOptions?: AnimationOptions, subInstructions?: ElementInstructionMap): AnimationTransitionInstruction { - const animationOptions = mergeAnimationOptions(this.ast.options || {}, options || {}); + const errors: any[] = []; + + const transitionAnimationParams = this.ast.options && this.ast.options.params || EMPTY_OBJECT; + const currentAnimationParams = currentOptions && currentOptions.params || EMPTY_OBJECT; + const currentStateStyles = this.buildStyles(currentState, currentAnimationParams, errors); + const nextAnimationParams = nextOptions && nextOptions.params || EMPTY_OBJECT; + const nextStateStyles = this.buildStyles(nextState, nextAnimationParams, errors); - const backupStateStyles = this._stateStyles['*'] || {}; - const currentStateStyles = this._stateStyles[currentState] || backupStateStyles; - const nextStateStyles = this._stateStyles[nextState] || backupStateStyles; const queriedElements = new Set(); const preStyleMap = new Map(); const postStyleMap = new Map(); const isRemoval = nextState === 'void'; - const errors: any[] = []; + const animationOptions = {params: {...transitionAnimationParams, ...nextAnimationParams}}; + const timelines = buildAnimationTimelines( driver, element, this.ast.animation, currentStateStyles, nextStateStyles, animationOptions, subInstructions, errors); @@ -75,3 +88,31 @@ function oneOrMoreTransitionsMatch( matchFns: TransitionMatcherFn[], currentState: any, nextState: any): boolean { return matchFns.some(fn => fn(currentState, nextState)); } + +export class AnimationStateStyles { + constructor(private styles: StyleAst, private defaultParams: {[key: string]: any}) {} + + buildStyles(params: {[key: string]: any}, errors: string[]): ɵStyleData { + const finalStyles: ɵStyleData = {}; + const combinedParams = copyObj(this.defaultParams); + Object.keys(params).forEach(key => { + const value = params[key]; + if (value != null) { + combinedParams[key] = value; + } + }); + this.styles.styles.forEach(value => { + if (typeof value !== 'string') { + const styleObj = value as any; + Object.keys(styleObj).forEach(prop => { + let val = styleObj[prop]; + if (val.length > 1) { + val = interpolateParams(val, combinedParams, errors); + } + finalStyles[prop] = val; + }); + } + }); + return finalStyles; + } +} diff --git a/packages/animations/browser/src/dsl/animation_trigger.ts b/packages/animations/browser/src/dsl/animation_trigger.ts index 19cfdeaad5..9d820e25c2 100644 --- a/packages/animations/browser/src/dsl/animation_trigger.ts +++ b/packages/animations/browser/src/dsl/animation_trigger.ts @@ -7,10 +7,11 @@ */ import {ɵStyleData} from '@angular/animations'; -import {copyStyles} from '../util'; +import {copyStyles, interpolateParams} from '../util'; + +import {SequenceAst, StyleAst, TransitionAst, TriggerAst} from './animation_ast'; +import {AnimationStateStyles, AnimationTransitionFactory} from './animation_transition_factory'; -import {SequenceAst, TransitionAst, TriggerAst} from './animation_ast'; -import {AnimationTransitionFactory} from './animation_transition_factory'; /** * @experimental Animation support is experimental. @@ -25,16 +26,12 @@ export function buildTrigger(name: string, ast: TriggerAst): AnimationTrigger { export class AnimationTrigger { public transitionFactories: AnimationTransitionFactory[] = []; public fallbackTransition: AnimationTransitionFactory; - public states: {[stateName: string]: ɵStyleData} = {}; + public states: {[stateName: string]: AnimationStateStyles} = {}; constructor(public name: string, public ast: TriggerAst) { ast.states.forEach(ast => { - const obj = this.states[ast.name] = {}; - ast.style.styles.forEach(styleTuple => { - if (typeof styleTuple == 'object') { - copyStyles(styleTuple as ɵStyleData, false, obj); - } - }); + const defaultParams = (ast.options && ast.options.params) || {}; + this.states[ast.name] = new AnimationStateStyles(ast.style, defaultParams); }); balanceProperties(this.states, 'true', '1'); @@ -53,10 +50,15 @@ export class AnimationTrigger { const entry = this.transitionFactories.find(f => f.match(currentState, nextState)); return entry || null; } + + matchStyles(currentState: any, params: {[key: string]: any}, errors: any[]): ɵStyleData { + return this.fallbackTransition.buildStyles(currentState, params, errors); + } } function createFallbackTransition( - triggerName: string, states: {[stateName: string]: ɵStyleData}): AnimationTransitionFactory { + triggerName: string, + states: {[stateName: string]: AnimationStateStyles}): AnimationTransitionFactory { const matchers = [(fromState: any, toState: any) => true]; const animation = new SequenceAst([]); const transition = new TransitionAst(matchers, animation); diff --git a/packages/animations/browser/src/render/animation_engine_next.ts b/packages/animations/browser/src/render/animation_engine_next.ts index 4aa25fc0db..d7d598cbe1 100644 --- a/packages/animations/browser/src/render/animation_engine_next.ts +++ b/packages/animations/browser/src/render/animation_engine_next.ts @@ -29,8 +29,8 @@ export class AnimationEngine { this._transitionEngine = new TransitionAnimationEngine(driver, normalizer); this._timelineEngine = new TimelineAnimationEngine(driver, normalizer); - this._transitionEngine.onRemovalComplete = - (element: any, context: any) => { this.onRemovalComplete(element, context); } + this._transitionEngine.onRemovalComplete = (element: any, context: any) => + this.onRemovalComplete(element, context); } registerTrigger( diff --git a/packages/animations/browser/src/render/transition_animation_engine.ts b/packages/animations/browser/src/render/transition_animation_engine.ts index 1f41927a84..721f1bd75b 100644 --- a/packages/animations/browser/src/render/transition_animation_engine.ts +++ b/packages/animations/browser/src/render/transition_animation_engine.ts @@ -66,6 +66,8 @@ export class StateValue { public value: string; public options: AnimationOptions; + get params(): {[key: string]: any} { return this.options.params as{[key: string]: any}; } + constructor(input: any) { const isObj = input && input.hasOwnProperty('value'); const value = isObj ? input['value'] : input; @@ -213,7 +215,24 @@ export class AnimationTransitionNamespace { // The removal arc here is special cased because the same element is triggered // twice in the event that it contains animations on the outer/inner portions // of the host container - if (!isRemoval && fromState.value === toState.value) return; + if (!isRemoval && fromState.value === toState.value) { + // this means that despite the value not changing, some inner params + // have changed which means that the animation final styles need to be applied + if (!objEquals(fromState.params, toState.params)) { + const errors: any[] = []; + const fromStyles = trigger.matchStyles(fromState.value, fromState.params, errors); + const toStyles = trigger.matchStyles(toState.value, toState.params, errors); + if (errors.length) { + this._engine.reportError(errors); + } else { + this._engine.afterFlush(() => { + eraseStyles(element, fromStyles); + setStyles(element, toStyles); + }); + } + } + return; + } const playersOnElement: TransitionAnimationPlayer[] = getOrSetAsInMap(this._engine.playersByElement, element, []); @@ -490,6 +509,7 @@ export class TransitionAnimationEngine { // this method is designed to be overridden by the code that uses this engine public onRemovalComplete = (element: any, context: any) => {}; + /** @internal */ _onRemovalComplete(element: any, context: any) { this.onRemovalComplete(element, context); } constructor(public driver: AnimationDriver, private _normalizer: AnimationStyleNormalizer) {} @@ -663,7 +683,7 @@ export class TransitionAnimationEngine { private _buildInstruction(entry: QueueInstruction, subTimelines: ElementInstructionMap) { return entry.transition.build( this.driver, entry.element, entry.fromState.value, entry.toState.value, - entry.toState.options, subTimelines); + entry.fromState.options, entry.toState.options, subTimelines); } destroyInnerAnimations(containerElement: any) { @@ -780,6 +800,11 @@ export class TransitionAnimationEngine { } } + reportError(errors: string[]) { + throw new Error( + `Unable to process animations due to the following failed trigger transitions\n ${errors.join("\n")}`); + } + private _flushAnimations(cleanupFns: Function[], microtaskId: number): TransitionAnimationPlayer[] { const subTimelines = new ElementInstructionMap(); @@ -900,14 +925,14 @@ export class TransitionAnimationEngine { } if (erroneousTransitions.length) { - let msg = `Unable to process animations due to the following failed trigger transitions\n`; + const errors: string[] = []; erroneousTransitions.forEach(instruction => { - msg += `@${instruction.triggerName} has failed due to:\n`; - instruction.errors !.forEach(error => { msg += `- ${error}\n`; }); + errors.push(`@${instruction.triggerName} has failed due to:\n`); + instruction.errors !.forEach(error => errors.push(`- ${error}\n`)); }); allPlayers.forEach(player => player.destroy()); - throw new Error(msg); + this.reportError(errors); } // these can only be detected here since we have a map of all the elements @@ -1168,8 +1193,17 @@ export class TransitionAnimationEngine { if (details && details.removedBeforeQueried) return new NoopAnimationPlayer(); const isQueriedElement = element !== rootElement; - const previousPlayers = flattenGroupPlayers( - (allPreviousPlayersMap.get(element) || EMPTY_PLAYER_ARRAY).map(p => p.getRealPlayer())); + const previousPlayers = + flattenGroupPlayers((allPreviousPlayersMap.get(element) || EMPTY_PLAYER_ARRAY) + .map(p => p.getRealPlayer())) + .filter(p => { + // the `element` is not apart of the AnimationPlayer definition, but + // Mock/WebAnimations + // use the element within their implementation. This will be added in Angular5 to + // AnimationPlayer + const pp = p as any; + return pp.element ? pp.element === element : false; + }); const preStyles = preStylesMap.get(element); const postStyles = postStylesMap.get(element); @@ -1481,3 +1515,14 @@ function _flattenGroupPlayersRecur(players: AnimationPlayer[], finalPlayers: Ani } } } + +function objEquals(a: {[key: string]: any}, b: {[key: string]: any}): boolean { + const k1 = Object.keys(a); + const k2 = Object.keys(b); + if (k1.length != k2.length) return false; + for (let i = 0; i < k1.length; i++) { + const prop = k1[i]; + if (!b.hasOwnProperty(prop) || a[prop] !== b[prop]) return false; + } + return true; +} diff --git a/packages/animations/browser/src/util.ts b/packages/animations/browser/src/util.ts index a6dbb75f56..dbdfa4f213 100644 --- a/packages/animations/browser/src/util.ts +++ b/packages/animations/browser/src/util.ts @@ -9,6 +9,8 @@ import {AnimateTimings, AnimationMetadata, AnimationOptions, sequence, ɵStyleDa export const ONE_SECOND = 1000; +export const SUBSTITUTION_EXPR_START = '{{'; +export const SUBSTITUTION_EXPR_END = '}}'; export const ENTER_CLASSNAME = 'ng-enter'; export const LEAVE_CLASSNAME = 'ng-leave'; export const ENTER_SELECTOR = '.ng-enter'; @@ -151,10 +153,8 @@ export function normalizeAnimationEntry(steps: AnimationMetadata | AnimationMeta export function validateStyleParams( value: string | number, options: AnimationOptions, errors: any[]) { const params = options.params || {}; - if (typeof value !== 'string') return; - - const matches = value.toString().match(PARAM_REGEX); - if (matches) { + const matches = extractStyleParams(value); + if (matches.length) { matches.forEach(varName => { if (!params.hasOwnProperty(varName)) { errors.push( @@ -164,7 +164,22 @@ export function validateStyleParams( } } -const PARAM_REGEX = /\{\{\s*(.+?)\s*\}\}/g; +const PARAM_REGEX = + new RegExp(`${SUBSTITUTION_EXPR_START}\\s*(.+?)\\s*${SUBSTITUTION_EXPR_END}`, 'g'); +export function extractStyleParams(value: string | number): string[] { + let params: string[] = []; + if (typeof value === 'string') { + const val = value.toString(); + + let match: any; + while (match = PARAM_REGEX.exec(val)) { + params.push(match[1] as string); + } + PARAM_REGEX.lastIndex = 0; + } + return params; +} + export function interpolateParams( value: string | number, params: {[name: string]: any}, errors: any[]): string|number { const original = value.toString(); diff --git a/packages/animations/browser/test/dsl/animation_spec.ts b/packages/animations/browser/test/dsl/animation_spec.ts index d022379e12..bc6da97a92 100644 --- a/packages/animations/browser/test/dsl/animation_spec.ts +++ b/packages/animations/browser/test/dsl/animation_spec.ts @@ -5,7 +5,7 @@ * 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 {AUTO_STYLE, AnimationMetadata, AnimationMetadataType, animate, animation, group, keyframes, query, sequence, style, transition, trigger, useAnimation, ɵStyleData} from '@angular/animations'; +import {AUTO_STYLE, AnimationMetadata, AnimationMetadataType, animate, animation, group, keyframes, query, sequence, state, style, transition, trigger, useAnimation, ɵStyleData} from '@angular/animations'; import {AnimationOptions} from '@angular/core/src/animation/dsl'; import {Animation} from '../../src/dsl/animation'; @@ -174,6 +174,30 @@ export function main() { validateAndThrowAnimationSequence(steps2); }).toThrowError(/keyframes\(\) must be placed inside of a call to animate\(\)/); }); + + it('should throw if dynamic style substitutions are used without defaults within state() definitions', + () => { + const steps = [state('final', style({ + 'width': '{{ one }}px', + 'borderRadius': '{{ two }}px {{ three }}px', + }))]; + + expect(() => { validateAndThrowAnimationSequence(steps); }) + .toThrowError( + /state\("final", ...\) must define default values for all the following style substitutions: one, two, three/); + + const steps2 = [state( + 'panfinal', style({ + 'color': '{{ greyColor }}', + 'borderColor': '1px solid {{ greyColor }}', + 'backgroundColor': '{{ redColor }}', + }), + {params: {redColor: 'maroon'}})]; + + expect(() => { validateAndThrowAnimationSequence(steps2); }) + .toThrowError( + /state\("panfinal", ...\) must define default values for all the following style substitutions: greyColor/); + }); }); describe('keyframe building', () => { @@ -427,17 +451,17 @@ export function main() { it('should throw an error when an input variable is not provided when invoked and is not a default value', () => { - expect(() => {invokeAnimationSequence(rootElement, [style({color: '{{ color }}'})])}) + expect(() => invokeAnimationSequence(rootElement, [style({color: '{{ color }}'})])) .toThrowError(/Please provide a value for the animation param color/); expect( - () => {invokeAnimationSequence( + () => invokeAnimationSequence( rootElement, [ style({color: '{{ start }}'}), animate('{{ time }}', style({color: '{{ end }}'})), ], - buildParams({start: 'blue', end: 'red'}))}) + buildParams({start: 'blue', end: 'red'}))) .toThrowError(/Please provide a value for the animation param time/); }); }); diff --git a/packages/animations/browser/test/dsl/animation_trigger_spec.ts b/packages/animations/browser/test/dsl/animation_trigger_spec.ts index eeb200005f..9db8e2404b 100644 --- a/packages/animations/browser/test/dsl/animation_trigger_spec.ts +++ b/packages/animations/browser/test/dsl/animation_trigger_spec.ts @@ -51,12 +51,14 @@ export function main() { describe('trigger usage', () => { it('should construct a trigger based on the states and transition data', () => { const result = makeTrigger('name', [ - state('on', style({width: 0})), state('off', style({width: 100})), - transition('on => off', animate(1000)), transition('off => on', animate(1000)) + state('on', style({width: 0})), + state('off', style({width: 100})), + transition('on => off', animate(1000)), + transition('off => on', animate(1000)), ]); - expect(result.states).toEqual({'on': {width: 0}, 'off': {width: 100}}); - + expect(result.states['on'].buildStyles({}, [])).toEqual({width: 0}); + expect(result.states['off'].buildStyles({}, [])).toEqual({width: 100}); expect(result.transitionFactories.length).toEqual(2); }); @@ -66,7 +68,9 @@ export function main() { transition('off => on', animate(1000)) ]); - expect(result.states).toEqual({'on': {width: 50}, 'off': {width: 50}}); + + expect(result.states['on'].buildStyles({}, [])).toEqual({width: 50}); + expect(result.states['off'].buildStyles({}, [])).toEqual({width: 50}); }); it('should find the first transition that matches', () => { @@ -145,7 +149,7 @@ export function main() { 'a => b', [style({height: '{{ a }}'}), animate(1000, style({height: '{{ b }}'}))], buildParams({a: '100px', b: '200px'}))]); - const trans = buildTransition(result, element, 'a', 'b', buildParams({a: '300px'})) !; + const trans = buildTransition(result, element, 'a', 'b', {}, buildParams({a: '300px'})) !; const keyframes = trans.timelines[0].keyframes; expect(keyframes).toEqual([{height: '300px', offset: 0}, {height: '200px', offset: 1}]); @@ -182,7 +186,7 @@ export function main() { const trans = buildTransition(result, element, false, true) !; expect(trans.timelines[0].keyframes).toEqual([ {offset: 0, color: 'red'}, {offset: 1, color: 'green'} - ]) + ]); }); it('should match `1` and `0` state styles on a `true <=> false` boolean transition given boolean values', @@ -195,7 +199,7 @@ export function main() { const trans = buildTransition(result, element, false, true) !; expect(trans.timelines[0].keyframes).toEqual([ {offset: 0, color: 'orange'}, {offset: 1, color: 'blue'} - ]) + ]); }); describe('aliases', () => { @@ -219,11 +223,12 @@ export function main() { function buildTransition( trigger: AnimationTrigger, element: any, fromState: any, toState: any, - params?: AnimationOptions): AnimationTransitionInstruction|null { + fromOptions?: AnimationOptions, toOptions?: AnimationOptions): AnimationTransitionInstruction| + null { const trans = trigger.matchTransition(fromState, toState) !; if (trans) { const driver = new MockAnimationDriver(); - return trans.build(driver, element, fromState, toState, params) !; + return trans.build(driver, element, fromState, toState, fromOptions, toOptions) !; } return null; } diff --git a/packages/animations/src/animation_metadata.ts b/packages/animations/src/animation_metadata.ts index cabf6dd429..d0f907508b 100755 --- a/packages/animations/src/animation_metadata.ts +++ b/packages/animations/src/animation_metadata.ts @@ -105,6 +105,7 @@ export interface AnimationTriggerMetadata extends AnimationMetadata { export interface AnimationStateMetadata extends AnimationMetadata { name: string; styles: AnimationStyleMetadata; + options?: {params: {[name: string]: any}}; } /** @@ -567,8 +568,10 @@ export function style( * * @experimental Animation support is experimental. */ -export function state(name: string, styles: AnimationStyleMetadata): AnimationStateMetadata { - return {type: AnimationMetadataType.State, name, styles}; +export function state( + name: string, styles: AnimationStyleMetadata, + options?: {params: {[name: string]: any}}): AnimationStateMetadata { + return {type: AnimationMetadataType.State, name, styles, options}; } /** diff --git a/packages/benchpress/index.ts b/packages/benchpress/index.ts index f91106e540..1b11272642 100644 --- a/packages/benchpress/index.ts +++ b/packages/benchpress/index.ts @@ -9,7 +9,7 @@ // Must be imported first, because Angular decorators throw on load. import 'reflect-metadata'; -export {InjectionToken, Injector, Provider, ReflectiveInjector} from '@angular/core'; +export {InjectionToken, Injector, Provider, ReflectiveInjector, StaticProvider} from '@angular/core'; export {Options} from './src/common_options'; export {MeasureValues} from './src/measure_values'; export {Metric} from './src/metric'; diff --git a/packages/benchpress/src/metric/perflog_metric.ts b/packages/benchpress/src/metric/perflog_metric.ts index 391a97ff90..f31fc6186d 100644 --- a/packages/benchpress/src/metric/perflog_metric.ts +++ b/packages/benchpress/src/metric/perflog_metric.ts @@ -20,7 +20,14 @@ import {PerfLogEvent, PerfLogFeatures, WebDriverExtension} from '../web_driver_e export class PerflogMetric extends Metric { static SET_TIMEOUT = new InjectionToken('PerflogMetric.setTimeout'); static PROVIDERS = [ - PerflogMetric, { + { + provide: PerflogMetric, + deps: [ + WebDriverExtension, PerflogMetric.SET_TIMEOUT, Options.MICRO_METRICS, Options.FORCE_GC, + Options.CAPTURE_FRAMES, Options.RECEIVED_DATA, Options.REQUEST_COUNT + ] + }, + { provide: PerflogMetric.SET_TIMEOUT, useValue: (fn: Function, millis: number) => setTimeout(fn, millis) } @@ -156,7 +163,7 @@ export class PerflogMetric extends Metric { return result; } let resolve: (result: any) => void; - const promise = new Promise(res => { resolve = res; }); + const promise = new Promise<{[key: string]: number}>(res => { resolve = res; }); this._setTimeout(() => resolve(this._readUntilEndMark(markName, loopCount + 1)), 100); return promise; }); diff --git a/packages/benchpress/src/metric/user_metric.ts b/packages/benchpress/src/metric/user_metric.ts index 19b8d57dfe..fe253d913d 100644 --- a/packages/benchpress/src/metric/user_metric.ts +++ b/packages/benchpress/src/metric/user_metric.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Inject, Injectable} from '@angular/core'; +import {Inject, Injectable, StaticProvider} from '@angular/core'; import {Options} from '../common_options'; import {Metric} from '../metric'; @@ -14,7 +14,8 @@ import {WebDriverAdapter} from '../web_driver_adapter'; @Injectable() export class UserMetric extends Metric { - static PROVIDERS = [UserMetric]; + static PROVIDERS = + [{provide: UserMetric, deps: [Options.USER_METRICS, WebDriverAdapter]}]; constructor( @Inject(Options.USER_METRICS) private _userMetrics: {[key: string]: string}, diff --git a/packages/benchpress/src/reporter/console_reporter.ts b/packages/benchpress/src/reporter/console_reporter.ts index 72fd545175..1659640010 100644 --- a/packages/benchpress/src/reporter/console_reporter.ts +++ b/packages/benchpress/src/reporter/console_reporter.ts @@ -22,7 +22,11 @@ export class ConsoleReporter extends Reporter { static PRINT = new InjectionToken('ConsoleReporter.print'); static COLUMN_WIDTH = new InjectionToken('ConsoleReporter.columnWidth'); static PROVIDERS = [ - ConsoleReporter, {provide: ConsoleReporter.COLUMN_WIDTH, useValue: 18}, { + { + provide: ConsoleReporter, + deps: [ConsoleReporter.COLUMN_WIDTH, SampleDescription, ConsoleReporter.PRINT] + }, + {provide: ConsoleReporter.COLUMN_WIDTH, useValue: 18}, { provide: ConsoleReporter.PRINT, useValue: function(v: any) { // tslint:disable-next-line:no-console diff --git a/packages/benchpress/src/reporter/json_file_reporter.ts b/packages/benchpress/src/reporter/json_file_reporter.ts index 2a8d0fb629..aaca71e916 100644 --- a/packages/benchpress/src/reporter/json_file_reporter.ts +++ b/packages/benchpress/src/reporter/json_file_reporter.ts @@ -22,7 +22,13 @@ import {formatStats, sortedProps} from './util'; @Injectable() export class JsonFileReporter extends Reporter { static PATH = new InjectionToken('JsonFileReporter.path'); - static PROVIDERS = [JsonFileReporter, {provide: JsonFileReporter.PATH, useValue: '.'}]; + static PROVIDERS = [ + { + provide: JsonFileReporter, + deps: [SampleDescription, JsonFileReporter.PATH, Options.WRITE_FILE, Options.NOW] + }, + {provide: JsonFileReporter.PATH, useValue: '.'} + ]; constructor( private _description: SampleDescription, @Inject(JsonFileReporter.PATH) private _path: string, diff --git a/packages/benchpress/src/runner.ts b/packages/benchpress/src/runner.ts index 97a188f4d5..0d9a201f8d 100644 --- a/packages/benchpress/src/runner.ts +++ b/packages/benchpress/src/runner.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Provider, ReflectiveInjector} from '@angular/core'; +import {Injector, StaticProvider} from '@angular/core'; import {Options} from './common_options'; import {Metric} from './metric'; @@ -34,17 +34,17 @@ import {IOsDriverExtension} from './webdriver/ios_driver_extension'; * It provides defaults, creates the injector and calls the sampler. */ export class Runner { - constructor(private _defaultProviders: Provider[] = []) {} + constructor(private _defaultProviders: StaticProvider[] = []) {} sample({id, execute, prepare, microMetrics, providers, userMetrics}: { id: string, execute?: Function, prepare?: Function, microMetrics?: {[key: string]: string}, - providers?: Provider[], + providers?: StaticProvider[], userMetrics?: {[key: string]: string} }): Promise { - const sampleProviders: Provider[] = [ + const sampleProviders: StaticProvider[] = [ _DEFAULT_PROVIDERS, this._defaultProviders, {provide: Options.SAMPLE_ID, useValue: id}, {provide: Options.EXECUTE, useValue: execute} ]; @@ -61,7 +61,7 @@ export class Runner { sampleProviders.push(providers); } - const inj = ReflectiveInjector.resolveAndCreate(sampleProviders); + const inj = Injector.create(sampleProviders); const adapter: WebDriverAdapter = inj.get(WebDriverAdapter); return Promise @@ -75,7 +75,7 @@ export class Runner { // Only WebDriverAdapter is reused. // TODO vsavkin consider changing it when toAsyncFactory is added back or when child // injectors are handled better. - const injector = ReflectiveInjector.resolveAndCreate([ + const injector = Injector.create([ sampleProviders, {provide: Options.CAPABILITIES, useValue: capabilities}, {provide: Options.USER_AGENT, useValue: userAgent}, {provide: WebDriverAdapter, useValue: adapter} diff --git a/packages/benchpress/src/sampler.ts b/packages/benchpress/src/sampler.ts index beb190852c..bd9d0fc868 100644 --- a/packages/benchpress/src/sampler.ts +++ b/packages/benchpress/src/sampler.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Inject, Injectable} from '@angular/core'; +import {Inject, Injectable, StaticProvider} from '@angular/core'; import {Options} from './common_options'; import {MeasureValues} from './measure_values'; @@ -26,8 +26,12 @@ import {WebDriverAdapter} from './web_driver_adapter'; */ @Injectable() export class Sampler { - static PROVIDERS = [Sampler]; - + static PROVIDERS = [{ + provide: Sampler, + deps: [ + WebDriverAdapter, Metric, Reporter, Validator, Options.PREPARE, Options.EXECUTE, Options.NOW + ] + }]; constructor( private _driver: WebDriverAdapter, private _metric: Metric, private _reporter: Reporter, private _validator: Validator, @Inject(Options.PREPARE) private _prepare: Function, diff --git a/packages/benchpress/src/validator/regression_slope_validator.ts b/packages/benchpress/src/validator/regression_slope_validator.ts index f0e2fa0980..8f590e2b24 100644 --- a/packages/benchpress/src/validator/regression_slope_validator.ts +++ b/packages/benchpress/src/validator/regression_slope_validator.ts @@ -21,7 +21,11 @@ export class RegressionSlopeValidator extends Validator { static SAMPLE_SIZE = new InjectionToken('RegressionSlopeValidator.sampleSize'); static METRIC = new InjectionToken('RegressionSlopeValidator.metric'); static PROVIDERS = [ - RegressionSlopeValidator, {provide: RegressionSlopeValidator.SAMPLE_SIZE, useValue: 10}, + { + provide: RegressionSlopeValidator, + deps: [RegressionSlopeValidator.SAMPLE_SIZE, RegressionSlopeValidator.METRIC] + }, + {provide: RegressionSlopeValidator.SAMPLE_SIZE, useValue: 10}, {provide: RegressionSlopeValidator.METRIC, useValue: 'scriptTime'} ]; diff --git a/packages/benchpress/src/validator/size_validator.ts b/packages/benchpress/src/validator/size_validator.ts index 711eb9edbb..7d546c186a 100644 --- a/packages/benchpress/src/validator/size_validator.ts +++ b/packages/benchpress/src/validator/size_validator.ts @@ -17,7 +17,10 @@ import {Validator} from '../validator'; @Injectable() export class SizeValidator extends Validator { static SAMPLE_SIZE = new InjectionToken('SizeValidator.sampleSize'); - static PROVIDERS = [SizeValidator, {provide: SizeValidator.SAMPLE_SIZE, useValue: 10}]; + static PROVIDERS = [ + {provide: SizeValidator, deps: [SizeValidator.SAMPLE_SIZE]}, + {provide: SizeValidator.SAMPLE_SIZE, useValue: 10} + ]; constructor(@Inject(SizeValidator.SAMPLE_SIZE) private _sampleSize: number) { super(); } diff --git a/packages/benchpress/src/webdriver/chrome_driver_extension.ts b/packages/benchpress/src/webdriver/chrome_driver_extension.ts index 0cf41cb38d..d1ceba67b1 100644 --- a/packages/benchpress/src/webdriver/chrome_driver_extension.ts +++ b/packages/benchpress/src/webdriver/chrome_driver_extension.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Inject, Injectable} from '@angular/core'; +import {Inject, Injectable, StaticProvider} from '@angular/core'; import {Options} from '../common_options'; import {WebDriverAdapter} from '../web_driver_adapter'; @@ -21,7 +21,10 @@ import {PerfLogEvent, PerfLogFeatures, WebDriverExtension} from '../web_driver_e */ @Injectable() export class ChromeDriverExtension extends WebDriverExtension { - static PROVIDERS = [ChromeDriverExtension]; + static PROVIDERS = [{ + provide: ChromeDriverExtension, + deps: [WebDriverAdapter, Options.USER_AGENT] + }]; private _majorChromeVersion: number; private _firstRun = true; diff --git a/packages/benchpress/src/webdriver/firefox_driver_extension.ts b/packages/benchpress/src/webdriver/firefox_driver_extension.ts index 0c3c360632..2d34742505 100644 --- a/packages/benchpress/src/webdriver/firefox_driver_extension.ts +++ b/packages/benchpress/src/webdriver/firefox_driver_extension.ts @@ -13,7 +13,7 @@ import {PerfLogEvent, PerfLogFeatures, WebDriverExtension} from '../web_driver_e @Injectable() export class FirefoxDriverExtension extends WebDriverExtension { - static PROVIDERS = [FirefoxDriverExtension]; + static PROVIDERS = [{provide: FirefoxDriverExtension, deps: [WebDriverAdapter]}]; private _profilerStarted: boolean; @@ -40,7 +40,7 @@ export class FirefoxDriverExtension extends WebDriverExtension { return this._driver.executeScript(script); } - readPerfLog(): Promise { + readPerfLog(): Promise { return this._driver.executeAsyncScript('var cb = arguments[0]; window.getProfile(cb);'); } diff --git a/packages/benchpress/src/webdriver/ios_driver_extension.ts b/packages/benchpress/src/webdriver/ios_driver_extension.ts index 8ceb3e2eeb..fefef6314c 100644 --- a/packages/benchpress/src/webdriver/ios_driver_extension.ts +++ b/packages/benchpress/src/webdriver/ios_driver_extension.ts @@ -13,7 +13,7 @@ import {PerfLogEvent, PerfLogFeatures, WebDriverExtension} from '../web_driver_e @Injectable() export class IOsDriverExtension extends WebDriverExtension { - static PROVIDERS = [IOsDriverExtension]; + static PROVIDERS = [{provide: IOsDriverExtension, deps: [WebDriverAdapter]}]; constructor(private _driver: WebDriverAdapter) { super(); } diff --git a/packages/benchpress/src/webdriver/selenium_webdriver_adapter.ts b/packages/benchpress/src/webdriver/selenium_webdriver_adapter.ts index efb1d91f4b..4aa0ad27e0 100644 --- a/packages/benchpress/src/webdriver/selenium_webdriver_adapter.ts +++ b/packages/benchpress/src/webdriver/selenium_webdriver_adapter.ts @@ -6,15 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ +import {StaticProvider} from '@angular/core'; + import {WebDriverAdapter} from '../web_driver_adapter'; + /** * Adapter for the selenium-webdriver. */ export class SeleniumWebDriverAdapter extends WebDriverAdapter { - static PROTRACTOR_PROVIDERS = [{ + static PROTRACTOR_PROVIDERS = [{ provide: WebDriverAdapter, - useFactory: () => new SeleniumWebDriverAdapter((global).browser) + useFactory: () => new SeleniumWebDriverAdapter((global).browser), + deps: [] }]; constructor(private _driver: any) { super(); } diff --git a/packages/benchpress/test/metric/multi_metric_spec.ts b/packages/benchpress/test/metric/multi_metric_spec.ts index 53f2892bbd..3d8ff95467 100644 --- a/packages/benchpress/test/metric/multi_metric_spec.ts +++ b/packages/benchpress/test/metric/multi_metric_spec.ts @@ -7,12 +7,13 @@ */ import {AsyncTestCompleter, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; -import {Metric, MultiMetric, ReflectiveInjector} from '../../index'; + +import {Injector, Metric, MultiMetric} from '../../index'; export function main() { function createMetric(ids: any[]) { - const m = ReflectiveInjector - .resolveAndCreate([ + const m = Injector + .create([ ids.map(id => ({provide: id, useValue: new MockMetric(id)})), MultiMetric.provideWith(ids) ]) diff --git a/packages/benchpress/test/metric/perflog_metric_spec.ts b/packages/benchpress/test/metric/perflog_metric_spec.ts index 66b81cc160..68822092f9 100644 --- a/packages/benchpress/test/metric/perflog_metric_spec.ts +++ b/packages/benchpress/test/metric/perflog_metric_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {Provider} from '@angular/core'; +import {StaticProvider} from '@angular/core'; import {AsyncTestCompleter, beforeEach, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; -import {Metric, Options, PerfLogEvent, PerfLogFeatures, PerflogMetric, ReflectiveInjector, WebDriverExtension} from '../../index'; +import {Injector, Metric, Options, PerfLogEvent, PerfLogFeatures, PerflogMetric, WebDriverExtension} from '../../index'; import {TraceEventFactory} from '../trace_event_factory'; export function main() { @@ -33,7 +33,7 @@ export function main() { if (!microMetrics) { microMetrics = {}; } - const providers: Provider[] = [ + const providers: StaticProvider[] = [ Options.DEFAULT_PROVIDERS, PerflogMetric.PROVIDERS, {provide: Options.MICRO_METRICS, useValue: microMetrics}, { provide: PerflogMetric.SET_TIMEOUT, @@ -59,7 +59,7 @@ export function main() { if (requestCount != null) { providers.push({provide: Options.REQUEST_COUNT, useValue: requestCount}); } - return ReflectiveInjector.resolveAndCreate(providers).get(PerflogMetric); + return Injector.create(providers).get(PerflogMetric); } describe('perflog metric', () => { diff --git a/packages/benchpress/test/metric/user_metric_spec.ts b/packages/benchpress/test/metric/user_metric_spec.ts index fe0c382efd..6884620b68 100644 --- a/packages/benchpress/test/metric/user_metric_spec.ts +++ b/packages/benchpress/test/metric/user_metric_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Provider, ReflectiveInjector} from '@angular/core'; +import {Injector, StaticProvider} from '@angular/core'; import {AsyncTestCompleter, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; import {Options, PerfLogEvent, PerfLogFeatures, UserMetric, WebDriverAdapter} from '../../index'; @@ -25,12 +25,12 @@ export function main() { userMetrics = {}; } wdAdapter = new MockDriverAdapter(); - const providers: Provider[] = [ + const providers: StaticProvider[] = [ Options.DEFAULT_PROVIDERS, UserMetric.PROVIDERS, {provide: Options.USER_METRICS, useValue: userMetrics}, {provide: WebDriverAdapter, useValue: wdAdapter} ]; - return ReflectiveInjector.resolveAndCreate(providers).get(UserMetric); + return Injector.create(providers).get(UserMetric); } describe('user metric', () => { diff --git a/packages/benchpress/test/reporter/console_reporter_spec.ts b/packages/benchpress/test/reporter/console_reporter_spec.ts index bfe30e7547..7e3c15b6cb 100644 --- a/packages/benchpress/test/reporter/console_reporter_spec.ts +++ b/packages/benchpress/test/reporter/console_reporter_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {Provider} from '@angular/core'; +import {StaticProvider} from '@angular/core'; import {describe, expect, it} from '@angular/core/testing/src/testing_internal'; -import {ConsoleReporter, MeasureValues, ReflectiveInjector, SampleDescription} from '../../index'; +import {ConsoleReporter, Injector, MeasureValues, SampleDescription} from '../../index'; export function main() { describe('console reporter', () => { @@ -30,7 +30,7 @@ export function main() { if (sampleId == null) { sampleId = 'null'; } - const providers: Provider[] = [ + const providers: StaticProvider[] = [ ConsoleReporter.PROVIDERS, { provide: SampleDescription, useValue: new SampleDescription(sampleId, descriptions, metrics !) @@ -40,7 +40,7 @@ export function main() { if (columnWidth != null) { providers.push({provide: ConsoleReporter.COLUMN_WIDTH, useValue: columnWidth}); } - reporter = ReflectiveInjector.resolveAndCreate(providers).get(ConsoleReporter); + reporter = Injector.create(providers).get(ConsoleReporter); } it('should print the sample id, description and table header', () => { diff --git a/packages/benchpress/test/reporter/json_file_reporter_spec.ts b/packages/benchpress/test/reporter/json_file_reporter_spec.ts index 27c4b25584..cbdbf5d9d4 100644 --- a/packages/benchpress/test/reporter/json_file_reporter_spec.ts +++ b/packages/benchpress/test/reporter/json_file_reporter_spec.ts @@ -8,7 +8,7 @@ import {AsyncTestCompleter, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; -import {JsonFileReporter, MeasureValues, Options, ReflectiveInjector, SampleDescription} from '../../index'; +import {Injector, JsonFileReporter, MeasureValues, Options, SampleDescription} from '../../index'; export function main() { describe('file reporter', () => { @@ -34,7 +34,7 @@ export function main() { } } ]; - return ReflectiveInjector.resolveAndCreate(providers).get(JsonFileReporter); + return Injector.create(providers).get(JsonFileReporter); } it('should write all data into a file', diff --git a/packages/benchpress/test/reporter/multi_reporter_spec.ts b/packages/benchpress/test/reporter/multi_reporter_spec.ts index 1ee54f2cde..aa1502e36b 100644 --- a/packages/benchpress/test/reporter/multi_reporter_spec.ts +++ b/packages/benchpress/test/reporter/multi_reporter_spec.ts @@ -8,12 +8,12 @@ import {AsyncTestCompleter, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; -import {MeasureValues, MultiReporter, ReflectiveInjector, Reporter} from '../../index'; +import {Injector, MeasureValues, MultiReporter, Reporter} from '../../index'; export function main() { function createReporters(ids: any[]) { - const r = ReflectiveInjector - .resolveAndCreate([ + const r = Injector + .create([ ids.map(id => ({provide: id, useValue: new MockReporter(id)})), MultiReporter.provideWith(ids) ]) diff --git a/packages/benchpress/test/runner_spec.ts b/packages/benchpress/test/runner_spec.ts index 7c4ee88847..27683730c5 100644 --- a/packages/benchpress/test/runner_spec.ts +++ b/packages/benchpress/test/runner_spec.ts @@ -8,11 +8,11 @@ import {AsyncTestCompleter, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; -import {Injector, Metric, Options, ReflectiveInjector, Runner, SampleDescription, SampleState, Sampler, Validator, WebDriverAdapter} from '../index'; +import {Injector, Metric, Options, Runner, SampleDescription, SampleState, Sampler, Validator, WebDriverAdapter} from '../index'; export function main() { describe('runner', () => { - let injector: ReflectiveInjector; + let injector: Injector; let runner: Runner; function createRunner(defaultProviders?: any[]): Runner { @@ -22,7 +22,7 @@ export function main() { runner = new Runner([ defaultProviders, { provide: Sampler, - useFactory: (_injector: ReflectiveInjector) => { + useFactory: (_injector: Injector) => { injector = _injector; return new MockSampler(); }, diff --git a/packages/benchpress/test/sampler_spec.ts b/packages/benchpress/test/sampler_spec.ts index e7e8339ee7..82ca5d6d64 100644 --- a/packages/benchpress/test/sampler_spec.ts +++ b/packages/benchpress/test/sampler_spec.ts @@ -8,7 +8,7 @@ import {AsyncTestCompleter, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; -import {MeasureValues, Metric, Options, ReflectiveInjector, Reporter, Sampler, Validator, WebDriverAdapter} from '../index'; +import {Injector, MeasureValues, Metric, Options, Reporter, Sampler, Validator, WebDriverAdapter} from '../index'; export function main() { const EMPTY_EXECUTE = () => {}; @@ -44,7 +44,7 @@ export function main() { providers.push({provide: Options.PREPARE, useValue: prepare}); } - sampler = ReflectiveInjector.resolveAndCreate(providers).get(Sampler); + sampler = Injector.create(providers).get(Sampler); } it('should call the prepare and execute callbacks using WebDriverAdapter.waitFor', diff --git a/packages/benchpress/test/validator/regression_slope_validator_spec.ts b/packages/benchpress/test/validator/regression_slope_validator_spec.ts index 75be174bef..3b8c3e02ce 100644 --- a/packages/benchpress/test/validator/regression_slope_validator_spec.ts +++ b/packages/benchpress/test/validator/regression_slope_validator_spec.ts @@ -8,15 +8,15 @@ import {describe, expect, it} from '@angular/core/testing/src/testing_internal'; -import {MeasureValues, ReflectiveInjector, RegressionSlopeValidator} from '../../index'; +import {Injector, MeasureValues, RegressionSlopeValidator} from '../../index'; export function main() { describe('regression slope validator', () => { let validator: RegressionSlopeValidator; function createValidator({size, metric}: {size: number, metric: string}) { - validator = ReflectiveInjector - .resolveAndCreate([ + validator = Injector + .create([ RegressionSlopeValidator.PROVIDERS, {provide: RegressionSlopeValidator.METRIC, useValue: metric}, {provide: RegressionSlopeValidator.SAMPLE_SIZE, useValue: size} diff --git a/packages/benchpress/test/validator/size_validator_spec.ts b/packages/benchpress/test/validator/size_validator_spec.ts index 99bc4b9951..da5a14ad57 100644 --- a/packages/benchpress/test/validator/size_validator_spec.ts +++ b/packages/benchpress/test/validator/size_validator_spec.ts @@ -8,7 +8,7 @@ import {describe, expect, it} from '@angular/core/testing/src/testing_internal'; -import {MeasureValues, ReflectiveInjector, SizeValidator} from '../../index'; +import {Injector, MeasureValues, SizeValidator} from '../../index'; export function main() { describe('size validator', () => { @@ -16,8 +16,8 @@ export function main() { function createValidator(size: number) { validator = - ReflectiveInjector - .resolveAndCreate( + Injector + .create( [SizeValidator.PROVIDERS, {provide: SizeValidator.SAMPLE_SIZE, useValue: size}]) .get(SizeValidator); } diff --git a/packages/benchpress/test/web_driver_extension_spec.ts b/packages/benchpress/test/web_driver_extension_spec.ts index 37fdab29ab..178f851262 100644 --- a/packages/benchpress/test/web_driver_extension_spec.ts +++ b/packages/benchpress/test/web_driver_extension_spec.ts @@ -8,14 +8,14 @@ import {AsyncTestCompleter, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; -import {Options, ReflectiveInjector, WebDriverExtension} from '../index'; +import {Injector, Options, WebDriverExtension} from '../index'; export function main() { function createExtension(ids: any[], caps: any) { return new Promise((res, rej) => { try { - res(ReflectiveInjector - .resolveAndCreate([ + res(Injector + .create([ ids.map((id) => ({provide: id, useValue: new MockExtension(id)})), {provide: Options.CAPABILITIES, useValue: caps}, WebDriverExtension.provideFirstSupported(ids) diff --git a/packages/benchpress/test/webdriver/chrome_driver_extension_spec.ts b/packages/benchpress/test/webdriver/chrome_driver_extension_spec.ts index f12d8a8bd6..ebb17e3d9b 100644 --- a/packages/benchpress/test/webdriver/chrome_driver_extension_spec.ts +++ b/packages/benchpress/test/webdriver/chrome_driver_extension_spec.ts @@ -8,7 +8,7 @@ import {AsyncTestCompleter, describe, expect, iit, inject, it} from '@angular/core/testing/src/testing_internal'; -import {ChromeDriverExtension, Options, ReflectiveInjector, WebDriverAdapter, WebDriverExtension} from '../../index'; +import {ChromeDriverExtension, Injector, Options, WebDriverAdapter, WebDriverExtension} from '../../index'; import {TraceEventFactory} from '../trace_event_factory'; export function main() { @@ -41,8 +41,8 @@ export function main() { userAgent = CHROME45_USER_AGENT; } log = []; - extension = ReflectiveInjector - .resolveAndCreate([ + extension = Injector + .create([ ChromeDriverExtension.PROVIDERS, { provide: WebDriverAdapter, useValue: new MockDriverAdapter(log, perfRecords, messageMethod) diff --git a/packages/benchpress/test/webdriver/ios_driver_extension_spec.ts b/packages/benchpress/test/webdriver/ios_driver_extension_spec.ts index a91fc2982d..7ff0cd6b13 100644 --- a/packages/benchpress/test/webdriver/ios_driver_extension_spec.ts +++ b/packages/benchpress/test/webdriver/ios_driver_extension_spec.ts @@ -8,7 +8,7 @@ import {AsyncTestCompleter, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; -import {IOsDriverExtension, ReflectiveInjector, WebDriverAdapter, WebDriverExtension} from '../../index'; +import {IOsDriverExtension, Injector, WebDriverAdapter, WebDriverExtension} from '../../index'; import {TraceEventFactory} from '../trace_event_factory'; export function main() { @@ -24,8 +24,8 @@ export function main() { } log = []; extension = - ReflectiveInjector - .resolveAndCreate([ + Injector + .create([ IOsDriverExtension.PROVIDERS, {provide: WebDriverAdapter, useValue: new MockDriverAdapter(log, perfRecords)} ]) diff --git a/packages/common/http/src/request.ts b/packages/common/http/src/request.ts index 7673e71f0e..a77da15f7d 100644 --- a/packages/common/http/src/request.ts +++ b/packages/common/http/src/request.ts @@ -15,8 +15,11 @@ import {HttpParams} from './params'; * All values are optional and will override default values if provided. */ interface HttpRequestInit { - headers?: HttpHeaders, reportProgress?: boolean, params?: HttpParams, - responseType?: 'arraybuffer'|'blob'|'json'|'text', withCredentials?: boolean, + headers?: HttpHeaders; + reportProgress?: boolean; + params?: HttpParams; + responseType?: 'arraybuffer'|'blob'|'json'|'text'; + withCredentials?: boolean; } /** diff --git a/packages/common/http/src/response.ts b/packages/common/http/src/response.ts index 0850505cc5..a56e4da14d 100644 --- a/packages/common/http/src/response.ts +++ b/packages/common/http/src/response.ts @@ -120,7 +120,10 @@ export interface HttpUserEvent { type: HttpEventType.User; } * * @experimental */ -export interface HttpJsonParseError { error: Error, text: string, } +export interface HttpJsonParseError { + error: Error; + text: string; +} /** * Union type for all possible events on the response stream. @@ -233,7 +236,7 @@ export class HttpHeaderResponse extends HttpResponseBase { status: update.status !== undefined ? update.status : this.status, statusText: update.statusText || this.statusText, url: update.url || this.url || undefined, - }) + }); } } diff --git a/packages/common/http/src/xhr.ts b/packages/common/http/src/xhr.ts index 8f35a98ffe..88ad2e2658 100644 --- a/packages/common/http/src/xhr.ts +++ b/packages/common/http/src/xhr.ts @@ -268,27 +268,26 @@ export class HttpXhrBackend implements HttpBackend { // The upload progress event handler, which is only registered if // progress events are enabled. - const onUpProgress = - (event: ProgressEvent) => { - // Upload progress events are simpler. Begin building the progress - // event. - let progress: HttpUploadProgressEvent = { - type: HttpEventType.UploadProgress, - loaded: event.loaded, - }; + const onUpProgress = (event: ProgressEvent) => { + // Upload progress events are simpler. Begin building the progress + // event. + let progress: HttpUploadProgressEvent = { + type: HttpEventType.UploadProgress, + loaded: event.loaded, + }; - // If the total number of bytes being uploaded is available, include - // it. - if (event.lengthComputable) { - progress.total = event.total; - } + // If the total number of bytes being uploaded is available, include + // it. + if (event.lengthComputable) { + progress.total = event.total; + } - // Send the event. - observer.next(progress); - } + // Send the event. + observer.next(progress); + }; - // By default, register for load and error events. - xhr.addEventListener('load', onLoad); + // By default, register for load and error events. + xhr.addEventListener('load', onLoad); xhr.addEventListener('error', onError); // Progress events are only enabled if requested. diff --git a/packages/common/http/test/jsonp_spec.ts b/packages/common/http/test/jsonp_spec.ts index 1a7bb49254..141cd544db 100644 --- a/packages/common/http/test/jsonp_spec.ts +++ b/packages/common/http/test/jsonp_spec.ts @@ -56,11 +56,11 @@ export function main() { }); describe('throws an error', () => { it('when request method is not JSONP', - () => {expect(() => backend.handle(SAMPLE_REQ.clone({method: 'GET'}))) - .toThrowError(JSONP_ERR_WRONG_METHOD)}); + () => expect(() => backend.handle(SAMPLE_REQ.clone({method: 'GET'}))) + .toThrowError(JSONP_ERR_WRONG_METHOD)); it('when response type is not json', - () => {expect(() => backend.handle(SAMPLE_REQ.clone({responseType: 'text'}))) - .toThrowError(JSONP_ERR_WRONG_RESPONSE_TYPE)}); + () => expect(() => backend.handle(SAMPLE_REQ.clone({responseType: 'text'}))) + .toThrowError(JSONP_ERR_WRONG_RESPONSE_TYPE)); it('when callback is never called', (done: DoneFn) => { backend.handle(SAMPLE_REQ).subscribe(undefined, (err: HttpErrorResponse) => { expect(err.status).toBe(0); @@ -69,7 +69,7 @@ export function main() { done(); }); document.mockLoad(); - }) + }); }); }); } diff --git a/packages/common/http/test/params_spec.ts b/packages/common/http/test/params_spec.ts index 27c2d83d76..8bc167836b 100644 --- a/packages/common/http/test/params_spec.ts +++ b/packages/common/http/test/params_spec.ts @@ -13,7 +13,7 @@ export function main() { describe('initialization', () => { it('should be empty at construction', () => { const body = new HttpParams(); - expect(body.toString()).toEqual('') + expect(body.toString()).toEqual(''); }); it('should parse an existing url', () => { diff --git a/packages/common/http/test/xhr_spec.ts b/packages/common/http/test/xhr_spec.ts index cdc3162a95..fc9dff6422 100644 --- a/packages/common/http/test/xhr_spec.ts +++ b/packages/common/http/test/xhr_spec.ts @@ -271,7 +271,7 @@ export function main() { done(); }); factory.mock.mockFlush(200, 'OK', 'Test'); - }) + }); }); describe('corrects for quirks', () => { it('by normalizing 1223 status to 204', (done: DoneFn) => { diff --git a/packages/common/http/test/xsrf_spec.ts b/packages/common/http/test/xsrf_spec.ts index f59b7df84d..1c5c235432 100644 --- a/packages/common/http/test/xsrf_spec.ts +++ b/packages/common/http/test/xsrf_spec.ts @@ -64,7 +64,7 @@ export function main() { }); describe('HttpXsrfCookieExtractor', () => { let document: {[key: string]: string}; - let extractor: HttpXsrfCookieExtractor + let extractor: HttpXsrfCookieExtractor; beforeEach(() => { document = { cookie: 'XSRF-TOKEN=test', diff --git a/packages/common/http/testing/src/backend.ts b/packages/common/http/testing/src/backend.ts index 1cb5e2b125..3de345b984 100644 --- a/packages/common/http/testing/src/backend.ts +++ b/packages/common/http/testing/src/backend.ts @@ -126,7 +126,7 @@ export class HttpClientTestingBackend implements HttpBackend, HttpTestingControl const requests = open.map(testReq => { const url = testReq.request.urlWithParams.split('?')[0]; const method = testReq.request.method; - return `${method} ${url}` + return `${method} ${url}`; }) .join(', '); throw new Error(`Expected no open requests, found ${open.length}: ${requests}`); diff --git a/packages/common/src/directives/ng_component_outlet.ts b/packages/common/src/directives/ng_component_outlet.ts index 34e32c8e71..3c018a7803 100644 --- a/packages/common/src/directives/ng_component_outlet.ts +++ b/packages/common/src/directives/ng_component_outlet.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {ComponentFactoryResolver, ComponentRef, Directive, Injector, Input, NgModuleFactory, NgModuleRef, OnChanges, OnDestroy, Provider, SimpleChanges, Type, ViewContainerRef} from '@angular/core'; +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. diff --git a/packages/common/src/directives/ng_template_outlet.ts b/packages/common/src/directives/ng_template_outlet.ts index ccfa8ecee4..800eed23ec 100644 --- a/packages/common/src/directives/ng_template_outlet.ts +++ b/packages/common/src/directives/ng_template_outlet.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core'; +import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChange, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core'; /** * @ngModule CommonModule @@ -49,13 +49,58 @@ export class NgTemplateOutlet implements OnChanges { set ngOutletContext(context: Object) { this.ngTemplateOutletContext = context; } ngOnChanges(changes: SimpleChanges) { - if (this._viewRef) { - this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._viewRef)); - } + const recreateView = this._shouldRecreateView(changes); - if (this.ngTemplateOutlet) { - this._viewRef = this._viewContainerRef.createEmbeddedView( - this.ngTemplateOutlet, this.ngTemplateOutletContext); + 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 + * + * To 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/packages/common/src/pipes/date_pipe.ts b/packages/common/src/pipes/date_pipe.ts index 12a5aa7340..aec4cee835 100644 --- a/packages/common/src/pipes/date_pipe.ts +++ b/packages/common/src/pipes/date_pipe.ts @@ -43,8 +43,8 @@ const ISO8601_DATE_REGEX = * | 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 (13) | jj (13) | - * | hour12 | h | - | - | - | h (1 PM) | hh (01 PM)| + * | 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) | diff --git a/packages/common/test/directives/ng_component_outlet_spec.ts b/packages/common/test/directives/ng_component_outlet_spec.ts index 0d897a97c1..45f2c5e8b3 100644 --- a/packages/common/test/directives/ng_component_outlet_spec.ts +++ b/packages/common/test/directives/ng_component_outlet_spec.ts @@ -8,7 +8,7 @@ import {CommonModule} from '@angular/common'; import {NgComponentOutlet} from '@angular/common/src/directives/ng_component_outlet'; -import {Compiler, Component, ComponentRef, Inject, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleFactory, Optional, Provider, QueryList, ReflectiveInjector, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; +import {Compiler, Component, ComponentRef, Inject, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleFactory, Optional, QueryList, StaticProvider, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; import {TestBed, async, fakeAsync} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -96,7 +96,7 @@ export function main() { const uniqueValue = {}; fixture.componentInstance.currentComponent = InjectedComponent; - fixture.componentInstance.injector = ReflectiveInjector.resolveAndCreate( + fixture.componentInstance.injector = Injector.create( [{provide: TEST_TOKEN, useValue: uniqueValue}], fixture.componentRef.injector); fixture.detectChanges(); diff --git a/packages/common/test/directives/ng_template_outlet_spec.ts b/packages/common/test/directives/ng_template_outlet_spec.ts index 8f1d25ac95..13135629cd 100644 --- a/packages/common/test/directives/ng_template_outlet_spec.ts +++ b/packages/common/test/directives/ng_template_outlet_spec.ts @@ -7,7 +7,7 @@ */ import {CommonModule} from '@angular/common'; -import {Component, ContentChildren, Directive, NO_ERRORS_SCHEMA, QueryList, TemplateRef} from '@angular/core'; +import {Component, ContentChildren, Directive, Injectable, NO_ERRORS_SCHEMA, OnDestroy, QueryList, TemplateRef} from '@angular/core'; import {ComponentFixture, TestBed, async} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -26,11 +26,9 @@ export function main() { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [ - TestComponent, - CaptureTplRefs, - ], + declarations: [TestComponent, CaptureTplRefs, DestroyableCmpt], imports: [CommonModule], + providers: [DestroyedSpyService] }); }); @@ -125,9 +123,105 @@ export function main() { fixture.componentInstance.context = {shawshank: 'was here'}; detectChangesAndExpectText('was here'); })); + + it('should update but not destroy embedded view when context values change', () => { + const template = + `:{{foo}}` + + ``; + + fixture = createTestComponent(template); + const spyService = fixture.debugElement.injector.get(DestroyedSpyService); + + detectChangesAndExpectText('Content to destroy:bar'); + expect(spyService.destroyed).toBeFalsy(); + + fixture.componentInstance.value = 'baz'; + detectChangesAndExpectText('Content to destroy:baz'); + expect(spyService.destroyed).toBeFalsy(); + }); + + it('should recreate embedded view when context shape changes', () => { + const template = + `:{{foo}}` + + ``; + + fixture = createTestComponent(template); + const spyService = fixture.debugElement.injector.get(DestroyedSpyService); + + detectChangesAndExpectText('Content to destroy:bar'); + expect(spyService.destroyed).toBeFalsy(); + + fixture.componentInstance.context = {foo: 'baz', other: true}; + detectChangesAndExpectText('Content to destroy:baz'); + expect(spyService.destroyed).toBeTruthy(); + }); + + it('should destroy embedded view when context value changes and templateRef becomes undefined', + () => { + const template = + `:{{foo}}` + + ``; + + fixture = createTestComponent(template); + const spyService = fixture.debugElement.injector.get(DestroyedSpyService); + + detectChangesAndExpectText('Content to destroy:bar'); + expect(spyService.destroyed).toBeFalsy(); + + fixture.componentInstance.value = 'baz'; + detectChangesAndExpectText(''); + expect(spyService.destroyed).toBeTruthy(); + }); + + it('should not try to update null / undefined context when context changes but template stays the same', + () => { + const template = `{{foo}}` + + ``; + + fixture = createTestComponent(template); + detectChangesAndExpectText(''); + + fixture.componentInstance.value = 'baz'; + detectChangesAndExpectText(''); + }); + + it('should not try to update null / undefined context when template changes', () => { + const template = `{{foo}}` + + `{{foo}}` + + ``; + + fixture = createTestComponent(template); + detectChangesAndExpectText(''); + + fixture.componentInstance.value = 'baz'; + detectChangesAndExpectText(''); + }); + + it('should not try to update context on undefined view', () => { + const template = `{{foo}}` + + ``; + + fixture = createTestComponent(template); + detectChangesAndExpectText(''); + + fixture.componentInstance.value = 'baz'; + detectChangesAndExpectText(''); + }); }); } +@Injectable() +class DestroyedSpyService { + destroyed = false; +} + +@Component({selector: 'destroyable-cmpt', template: 'Content to destroy'}) +class DestroyableCmpt implements OnDestroy { + constructor(private _spyService: DestroyedSpyService) {} + + ngOnDestroy(): void { this._spyService.destroyed = true; } +} + @Directive({selector: 'tpl-refs', exportAs: 'tplRefs'}) class CaptureTplRefs { @ContentChildren(TemplateRef) tplRefs: QueryList>; @@ -137,6 +231,7 @@ class CaptureTplRefs { class TestComponent { currentTplRef: TemplateRef; context: any = {foo: 'bar'}; + value = 'bar'; } function createTestComponent(template: string): ComponentFixture { diff --git a/packages/common/test/pipes/date_pipe_spec.ts b/packages/common/test/pipes/date_pipe_spec.ts index 07a009d0dc..a4fb977011 100644 --- a/packages/common/test/pipes/date_pipe_spec.ts +++ b/packages/common/test/pipes/date_pipe_spec.ts @@ -124,9 +124,12 @@ export function main() { expectDateFormatAs(date, pattern, dateFixtures[pattern]); }); - Object.keys(isoStringWithoutTimeFixtures).forEach((pattern: string) => { - expectDateFormatAs(isoStringWithoutTime, pattern, isoStringWithoutTimeFixtures[pattern]); - }); + if (!browserDetection.isOldChrome) { + Object.keys(isoStringWithoutTimeFixtures).forEach((pattern: string) => { + expectDateFormatAs( + isoStringWithoutTime, pattern, isoStringWithoutTimeFixtures[pattern]); + }); + } expect(pipe.transform(date, 'Z')).toBeDefined(); }); diff --git a/packages/compiler-cli/index.ts b/packages/compiler-cli/index.ts index f242a5b64b..482b8ecba8 100644 --- a/packages/compiler-cli/index.ts +++ b/packages/compiler-cli/index.ts @@ -20,8 +20,7 @@ export {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Sp export * from './src/transformers/api'; export * from './src/transformers/entry_points'; -export {main as ngc} from './src/ngc'; -export {performCompilation} from './src/ngc'; +export {performCompilation} from './src/perform-compile'; // TODO(hansl): moving to Angular 4 need to update this API. export {NgTools_InternalApi_NG_2 as __NGTOOLS_PRIVATE_API_2} from './src/ngtools_api'; diff --git a/packages/compiler-cli/package.json b/packages/compiler-cli/package.json index ef72109112..ef1ce4b573 100644 --- a/packages/compiler-cli/package.json +++ b/packages/compiler-cli/package.json @@ -9,7 +9,7 @@ "ng-xi18n": "./src/extract_i18n.js" }, "dependencies": { - "@angular/tsc-wrapped": "5.0.0-beta.1", + "@angular/tsc-wrapped": "5.0.0-beta.2", "reflect-metadata": "^0.1.2", "minimist": "^1.2.0" }, diff --git a/packages/compiler-cli/src/codegen.ts b/packages/compiler-cli/src/codegen.ts index cf8ea0a07e..f861db7382 100644 --- a/packages/compiler-cli/src/codegen.ts +++ b/packages/compiler-cli/src/codegen.ts @@ -96,7 +96,7 @@ export class CodeGenerator { } } if (!transContent) { - missingTranslation = MissingTranslationStrategy.Ignore + missingTranslation = MissingTranslationStrategy.Ignore; } const {compiler: aotCompiler} = compiler.createAotCompiler(ngCompilerHost, { translations: transContent, 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/diagnostics/check_types.ts b/packages/compiler-cli/src/diagnostics/check_types.ts index 3a2030043d..0029f2fe60 100644 --- a/packages/compiler-cli/src/diagnostics/check_types.ts +++ b/packages/compiler-cli/src/diagnostics/check_types.ts @@ -203,11 +203,9 @@ class TypeCheckingHost implements ts.CompilerHost { } writeFile: ts.WriteFileCallback = - () => { throw new Error('Unexpected write in diagnostic program'); } + () => { throw new Error('Unexpected write in diagnostic program'); }; - getCurrentDirectory(): string { - return this.host.getCurrentDirectory(); - } + getCurrentDirectory(): string { return this.host.getCurrentDirectory(); } getDirectories(path: string): string[] { return this.host.getDirectories(path); } diff --git a/packages/compiler-cli/src/ngc.ts b/packages/compiler-cli/src/ngc.ts index 6d21ff851d..658ed31d6e 100644 --- a/packages/compiler-cli/src/ngc.ts +++ b/packages/compiler-cli/src/ngc.ts @@ -10,185 +10,14 @@ import 'reflect-metadata'; import {isSyntaxError, syntaxError} from '@angular/compiler'; -import {MetadataBundler, createBundleIndexHost} from '@angular/tsc-wrapped'; import * as fs from 'fs'; import * as path from 'path'; -import * as ts from 'typescript'; -import * as api from './transformers/api'; -import * as ng from './transformers/entry_points'; - -const TS_EXT = /\.ts$/; - -type Diagnostics = ts.Diagnostic[] | api.Diagnostic[]; - -function isTsDiagnostics(diagnostics: any): diagnostics is ts.Diagnostic[] { - return diagnostics && diagnostics[0] && (diagnostics[0].file || diagnostics[0].messageText); -} - -function formatDiagnostics(cwd: string, diags: Diagnostics): string { - if (diags && diags.length) { - if (isTsDiagnostics(diags)) { - return ts.formatDiagnostics(diags, { - getCurrentDirectory: () => cwd, - getCanonicalFileName: fileName => fileName, - getNewLine: () => ts.sys.newLine - }); - } else { - return diags - .map(d => { - let res = api.DiagnosticCategory[d.category]; - if (d.span) { - res += - ` at ${d.span.start.file.url}(${d.span.start.line + 1},${d.span.start.col + 1})`; - } - if (d.span && d.span.details) { - res += `: ${d.span.details}, ${d.message}\n`; - } else { - res += `: ${d.message}\n`; - } - return res; - }) - .join(); - } - } else - return ''; -} - -function check(cwd: string, ...args: Diagnostics[]) { - if (args.some(diags => !!(diags && diags[0]))) { - throw syntaxError(args.map(diags => { - if (diags && diags[0]) { - return formatDiagnostics(cwd, diags); - } - }) - .filter(message => !!message) - .join('')); - } -} - -function syntheticError(message: string): ts.Diagnostic { - return { - file: null as any as ts.SourceFile, - start: 0, - length: 0, - messageText: message, - category: ts.DiagnosticCategory.Error, - code: 0 - }; -} - -export function readConfiguration( - project: string, basePath: string, checkFunc: (cwd: string, ...args: any[]) => void = check, - existingOptions?: ts.CompilerOptions) { - // Allow a directory containing tsconfig.json as the project value - // Note, TS@next returns an empty array, while earlier versions throw - const projectFile = - fs.lstatSync(project).isDirectory() ? path.join(project, 'tsconfig.json') : project; - let {config, error} = ts.readConfigFile(projectFile, ts.sys.readFile); - - if (error) checkFunc(basePath, [error]); - const parseConfigHost = { - useCaseSensitiveFileNames: true, - fileExists: fs.existsSync, - readDirectory: ts.sys.readDirectory, - readFile: ts.sys.readFile - }; - const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, existingOptions); - - checkFunc(basePath, parsed.errors); - - // Default codegen goes to the current directory - // Parsed options are already converted to absolute paths - const ngOptions = config.angularCompilerOptions || {}; - // Ignore the genDir option - ngOptions.genDir = basePath; - - return {parsed, ngOptions}; -} - -function getProjectDirectory(project: string): string { - let isFile: boolean; - try { - isFile = fs.lstatSync(project).isFile(); - } catch (e) { - // Project doesn't exist. Assume it is a file has an extension. This case happens - // when the project file is passed to set basePath but no tsconfig.json file exists. - // It is used in tests to ensure that the options can be passed in without there being - // an actual config file. - isFile = path.extname(project) !== ''; - } - - // If project refers to a file, the project directory is the file's parent directory - // otherwise project is the project directory. - return isFile ? path.dirname(project) : project; -} - -export function performCompilation( - basePath: string, files: string[], options: ts.CompilerOptions, ngOptions: any, - consoleError: (s: string) => void = console.error, - checkFunc: (cwd: string, ...args: any[]) => void = check, tsCompilerHost?: ts.CompilerHost) { - try { - ngOptions.basePath = basePath; - ngOptions.genDir = basePath; - - let host = tsCompilerHost || ts.createCompilerHost(options, true); - host.realpath = p => p; - - const rootFileNames = files.map(f => path.normalize(f)); - - const addGeneratedFileName = - (fileName: string) => { - if (fileName.startsWith(basePath) && TS_EXT.exec(fileName)) { - rootFileNames.push(fileName); - } - } - - if (ngOptions.flatModuleOutFile && !ngOptions.skipMetadataEmit) { - const {host: bundleHost, indexName, errors} = - createBundleIndexHost(ngOptions, rootFileNames, host); - if (errors) checkFunc(basePath, errors); - if (indexName) addGeneratedFileName(indexName); - host = bundleHost; - } - - const ngHostOptions = {...options, ...ngOptions}; - const ngHost = ng.createHost({tsHost: host, options: ngHostOptions}); - - const ngProgram = - ng.createProgram({rootNames: rootFileNames, host: ngHost, options: ngHostOptions}); - - // Check parameter diagnostics - checkFunc(basePath, ngProgram.getTsOptionDiagnostics(), ngProgram.getNgOptionDiagnostics()); - - // Check syntactic diagnostics - checkFunc(basePath, ngProgram.getTsSyntacticDiagnostics()); - - // Check TypeScript semantic and Angular structure diagnostics - checkFunc( - basePath, ngProgram.getTsSemanticDiagnostics(), ngProgram.getNgStructuralDiagnostics()); - - // Check Angular semantic diagnostics - checkFunc(basePath, ngProgram.getNgSemanticDiagnostics()); - - ngProgram.emit({ - emitFlags: api.EmitFlags.Default | - ((ngOptions.skipMetadataEmit || ngOptions.flatModuleOutFile) ? 0 : api.EmitFlags.Metadata) - }); - } catch (e) { - if (isSyntaxError(e)) { - console.error(e.message); - consoleError(e.message); - return 1; - } - } - - return 0; -} +import {performCompilation, readConfiguration, throwOnDiagnostics} from './perform-compile'; export function main( args: string[], consoleError: (s: string) => void = console.error, - checkFunc: (cwd: string, ...args: any[]) => void = check): number { + checkFunc: (cwd: string, ...args: any[]) => void = throwOnDiagnostics): number { try { const parsedArgs = require('minimist')(args); const project = parsedArgs.p || parsedArgs.project || '.'; 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/perform-compile.ts b/packages/compiler-cli/src/perform-compile.ts new file mode 100644 index 0000000000..55feddb347 --- /dev/null +++ b/packages/compiler-cli/src/perform-compile.ts @@ -0,0 +1,192 @@ +/** + * @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 {isSyntaxError, syntaxError} from '@angular/compiler'; +import {MetadataBundler, createBundleIndexHost} from '@angular/tsc-wrapped'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; +import * as api from './transformers/api'; +import * as ng from './transformers/entry_points'; + +const TS_EXT = /\.ts$/; + +export type Diagnostics = ts.Diagnostic[] | api.Diagnostic[]; + +function isTsDiagnostics(diagnostics: any): diagnostics is ts.Diagnostic[] { + return diagnostics && diagnostics[0] && (diagnostics[0].file || diagnostics[0].messageText); +} + +function formatDiagnostics(cwd: string, diags: Diagnostics): string { + if (diags && diags.length) { + if (isTsDiagnostics(diags)) { + return ts.formatDiagnostics(diags, { + getCurrentDirectory: () => cwd, + getCanonicalFileName: fileName => fileName, + getNewLine: () => ts.sys.newLine + }); + } else { + return diags + .map(d => { + let res = api.DiagnosticCategory[d.category]; + if (d.span) { + res += + ` at ${d.span.start.file.url}(${d.span.start.line + 1},${d.span.start.col + 1})`; + } + if (d.span && d.span.details) { + res += `: ${d.span.details}, ${d.message}\n`; + } else { + res += `: ${d.message}\n`; + } + return res; + }) + .join(); + } + } else + return ''; +} + +/** + * Throw a syntax error exception with a message formatted for output + * if the args parameter contains diagnostics errors. + * + * @param cwd The directory to report error as relative to. + * @param args A list of potentially empty diagnostic errors. + */ +export function throwOnDiagnostics(cwd: string, ...args: Diagnostics[]) { + if (args.some(diags => !!(diags && diags[0]))) { + throw syntaxError(args.map(diags => { + if (diags && diags[0]) { + return formatDiagnostics(cwd, diags); + } + }) + .filter(message => !!message) + .join('')); + } +} + +function syntheticError(message: string): ts.Diagnostic { + return { + file: null as any as ts.SourceFile, + start: 0, + length: 0, + messageText: message, + category: ts.DiagnosticCategory.Error, + code: 0 + }; +} + +export function readConfiguration( + project: string, basePath: string, + checkFunc: (cwd: string, ...args: any[]) => void = throwOnDiagnostics, + existingOptions?: ts.CompilerOptions) { + // Allow a directory containing tsconfig.json as the project value + // Note, TS@next returns an empty array, while earlier versions throw + const projectFile = + fs.lstatSync(project).isDirectory() ? path.join(project, 'tsconfig.json') : project; + let {config, error} = ts.readConfigFile(projectFile, ts.sys.readFile); + + if (error) checkFunc(basePath, [error]); + const parseConfigHost = { + useCaseSensitiveFileNames: true, + fileExists: fs.existsSync, + readDirectory: ts.sys.readDirectory, + readFile: ts.sys.readFile + }; + const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, existingOptions); + + checkFunc(basePath, parsed.errors); + + // Default codegen goes to the current directory + // Parsed options are already converted to absolute paths + const ngOptions = config.angularCompilerOptions || {}; + // Ignore the genDir option + ngOptions.genDir = basePath; + + return {parsed, ngOptions}; +} + +function getProjectDirectory(project: string): string { + let isFile: boolean; + try { + isFile = fs.lstatSync(project).isFile(); + } catch (e) { + // Project doesn't exist. Assume it is a file has an extension. This case happens + // when the project file is passed to set basePath but no tsconfig.json file exists. + // It is used in tests to ensure that the options can be passed in without there being + // an actual config file. + isFile = path.extname(project) !== ''; + } + + // If project refers to a file, the project directory is the file's parent directory + // otherwise project is the project directory. + return isFile ? path.dirname(project) : project; +} + +export function performCompilation( + basePath: string, files: string[], options: ts.CompilerOptions, ngOptions: any, + consoleError: (s: string) => void = console.error, + checkFunc: (cwd: string, ...args: any[]) => void = throwOnDiagnostics, + tsCompilerHost?: ts.CompilerHost) { + try { + ngOptions.basePath = basePath; + ngOptions.genDir = basePath; + + let host = tsCompilerHost || ts.createCompilerHost(options, true); + host.realpath = p => p; + + const rootFileNames = files.map(f => path.normalize(f)); + + const addGeneratedFileName = (fileName: string) => { + if (fileName.startsWith(basePath) && TS_EXT.exec(fileName)) { + rootFileNames.push(fileName); + } + }; + + if (ngOptions.flatModuleOutFile && !ngOptions.skipMetadataEmit) { + const {host: bundleHost, indexName, errors} = + createBundleIndexHost(ngOptions, rootFileNames, host); + if (errors) checkFunc(basePath, errors); + if (indexName) addGeneratedFileName(indexName); + host = bundleHost; + } + + const ngHostOptions = {...options, ...ngOptions}; + const ngHost = ng.createHost({tsHost: host, options: ngHostOptions}); + + const ngProgram = + ng.createProgram({rootNames: rootFileNames, host: ngHost, options: ngHostOptions}); + + // Check parameter diagnostics + checkFunc(basePath, ngProgram.getTsOptionDiagnostics(), ngProgram.getNgOptionDiagnostics()); + + // Check syntactic diagnostics + checkFunc(basePath, ngProgram.getTsSyntacticDiagnostics()); + + // Check TypeScript semantic and Angular structure diagnostics + checkFunc( + basePath, ngProgram.getTsSemanticDiagnostics(), ngProgram.getNgStructuralDiagnostics()); + + // Check Angular semantic diagnostics + checkFunc(basePath, ngProgram.getNgSemanticDiagnostics()); + + ngProgram.emit({ + emitFlags: api.EmitFlags.Default | + ((ngOptions.skipMetadataEmit || ngOptions.flatModuleOutFile) ? 0 : api.EmitFlags.Metadata) + }); + } catch (e) { + if (isSyntaxError(e)) { + console.error(e.message); + consoleError(e.message); + return 1; + } + throw e; + } + + return 0; +} 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