From f0a55016af3d8a940af1acbee4712332a1448a3c Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Tue, 8 Aug 2017 11:41:12 -0700 Subject: [PATCH 01/83] fix(core): fix platform-browser-dynamic (#18576) follow-up for #18496 --- .../platform-browser-dynamic/src/platform-browser-dynamic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/platform-browser-dynamic/src/platform-browser-dynamic.ts b/packages/platform-browser-dynamic/src/platform-browser-dynamic.ts index 9b65cf4e95..929ae41858 100644 --- a/packages/platform-browser-dynamic/src/platform-browser-dynamic.ts +++ b/packages/platform-browser-dynamic/src/platform-browser-dynamic.ts @@ -18,7 +18,7 @@ export {VERSION} from './version'; * @experimental */ export const RESOURCE_CACHE_PROVIDER: Provider[] = - [{provide: ResourceLoader, useClass: CachedResourceLoader}]; + [{provide: ResourceLoader, useClass: CachedResourceLoader, deps: []}]; /** * @stable From 6f2038cc852fb31e5cdbab2f15cd3ab80bf3dac7 Mon Sep 17 00:00:00 2001 From: Chuck Jazdzewski Date: Tue, 8 Aug 2017 12:40:08 -0700 Subject: [PATCH 02/83] fix(compiler-cli): fix and re-enble expression lowering (#18570) Fixes issue uncovered by #18388 and re-enables expression lowering disabled by #18513. --- .../src/transformers/lower_expressions.ts | 56 +++++++++++++++++-- .../compiler-cli/src/transformers/program.ts | 3 +- packages/compiler-cli/test/ngc_spec.ts | 28 +++++++++- .../transformers/lower_expressions_spec.ts | 8 +-- 4 files changed, 80 insertions(+), 15 deletions(-) diff --git a/packages/compiler-cli/src/transformers/lower_expressions.ts b/packages/compiler-cli/src/transformers/lower_expressions.ts index 286ce3b4fa..65c4cd3aff 100644 --- a/packages/compiler-cli/src/transformers/lower_expressions.ts +++ b/packages/compiler-cli/src/transformers/lower_expressions.ts @@ -32,6 +32,24 @@ function toMap(items: T[], select: (item: T) => K): Map { return new Map(items.map<[K, T]>(i => [select(i), i])); } +// We will never lower expressions in a nested lexical scope so avoid entering them. +// This also avoids a bug in TypeScript 2.3 where the lexical scopes get out of sync +// when using visitEachChild. +function isLexicalScope(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.TypeLiteral: + case ts.SyntaxKind.ArrayType: + return true; + } + return false; +} + function transformSourceFile( sourceFile: ts.SourceFile, requests: RequestLocationMap, context: ts.TransformationContext): ts.SourceFile { @@ -56,11 +74,15 @@ function transformSourceFile( declarations.push({name, node}); return ts.createIdentifier(name); } - if (node.pos <= max && node.end >= min) return ts.visitEachChild(node, visitNode, context); - return node; + let result = node; + if (node.pos <= max && node.end >= min && !isLexicalScope(node)) { + result = ts.visitEachChild(node, visitNode, context); + } + return result; } - const result = ts.visitEachChild(node, visitNode, context); + const result = + (node.pos <= max && node.end >= min) ? ts.visitEachChild(node, visitNode, context) : node; if (declarations.length) { inserts.push({priorTo: result, declarations}); @@ -126,6 +148,29 @@ interface MetadataAndLoweringRequests { requests: RequestLocationMap; } +function shouldLower(node: ts.Node | undefined): boolean { + if (node) { + switch (node.kind) { + case ts.SyntaxKind.SourceFile: + case ts.SyntaxKind.Decorator: + // Lower expressions that are local to the module scope or + // in a decorator. + return true; + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.FunctionDeclaration: + // Don't lower expressions in a declaration. + return false; + case ts.SyntaxKind.VariableDeclaration: + // Avoid lowering expressions already in an exported variable declaration + return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) == 0; + } + return shouldLower(node.parent); + } + return true; +} + export class LowerMetadataCache implements RequestsMap { private collector: MetadataCollector; private metadataCache = new Map(); @@ -162,8 +207,9 @@ export class LowerMetadataCache implements RequestsMap { }; const substituteExpression = (value: MetadataValue, node: ts.Node): MetadataValue => { - if (node.kind === ts.SyntaxKind.ArrowFunction || - node.kind === ts.SyntaxKind.FunctionExpression) { + if ((node.kind === ts.SyntaxKind.ArrowFunction || + node.kind === ts.SyntaxKind.FunctionExpression) && + shouldLower(node)) { return replaceNode(node); } return value; diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index b2753555fa..3970aeb015 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -187,8 +187,7 @@ class AngularCompilerProgram implements Program { const before: ts.TransformerFactory[] = []; const after: ts.TransformerFactory[] = []; if (!this.options.disableExpressionLowering) { - // TODO(chuckj): fix and re-enable + tests - see https://github.com/angular/angular/pull/18388 - // before.push(getExpressionLoweringTransformFactory(this.metadataCache)); + before.push(getExpressionLoweringTransformFactory(this.metadataCache)); } if (!this.options.skipTemplateCodegen) { after.push(getAngularEmitterTransformFactory(this.generatedFiles)); diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index 0a994cf6e8..53320cc186 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -12,7 +12,7 @@ import * as path from 'path'; import * as ts from 'typescript'; import {main} from '../src/ngc'; -import {performCompilation} from '../src/perform-compile'; +import {performCompilation, readConfiguration} from '../src/perform-compile'; function getNgRootDir() { const moduleFilename = module.filename.replace(/\\/g, '/'); @@ -316,7 +316,7 @@ describe('ngc command-line', () => { .toBe(true); }); - xdescribe('expression lowering', () => { + describe('expression lowering', () => { beforeEach(() => { writeConfig(`{ "extends": "./tsconfig-base.json", @@ -424,13 +424,35 @@ describe('ngc command-line', () => { }) export class MyModule {} `); - expect(compile()).toEqual(0); + expect(compile()).toEqual(0, 'Compile failed'); const mymodulejs = path.resolve(outDir, 'mymodule.js'); const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8'); expect(mymoduleSource).toContain('var ɵ0 = function () { return new Foo(); }'); expect(mymoduleSource).toContain('export { ɵ0'); }); + + it('should not lower a lambda that is already exported', () => { + write('mymodule.ts', ` + import {CommonModule} from '@angular/common'; + import {NgModule} from '@angular/core'; + + class Foo {} + + export const factory = () => new Foo(); + + @NgModule({ + imports: [CommonModule], + providers: [{provide: 'someToken', useFactory: factory}] + }) + export class MyModule {} + `); + expect(compile()).toEqual(0); + + const mymodulejs = path.resolve(outDir, 'mymodule.js'); + const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8'); + expect(mymoduleSource).not.toContain('ɵ0'); + }); }); const shouldExist = (fileName: string) => { diff --git a/packages/compiler-cli/test/transformers/lower_expressions_spec.ts b/packages/compiler-cli/test/transformers/lower_expressions_spec.ts index 7588982b03..6b923ed671 100644 --- a/packages/compiler-cli/test/transformers/lower_expressions_spec.ts +++ b/packages/compiler-cli/test/transformers/lower_expressions_spec.ts @@ -51,11 +51,9 @@ function convert(annotatedSource: string) { for (const annotation of annotations) { const node = findNode(sourceFile, annotation.start, annotation.length); - expect(node).toBeDefined(); - if (node) { - const location = node.pos; - requests.set(location, {name: annotation.name, kind: node.kind, location, end: node.end}); - } + if (!node) throw new Error('Invalid test specification. Could not find the node to substitute'); + const location = node.pos; + requests.set(location, {name: annotation.name, kind: node.kind, location, end: node.end}); } const program = ts.createProgram( From 1e1833198dbe18bcc432ce3acf3aa808cf6b6884 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Tue, 8 Aug 2017 23:59:25 +0300 Subject: [PATCH 03/83] ci(aio): fix deploying to firebase (#18590) --- aio/scripts/deploy-to-firebase.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aio/scripts/deploy-to-firebase.sh b/aio/scripts/deploy-to-firebase.sh index 1542067de0..90ccedd954 100755 --- a/aio/scripts/deploy-to-firebase.sh +++ b/aio/scripts/deploy-to-firebase.sh @@ -78,7 +78,7 @@ echo "Build/deploy mode : $deployEnv" echo "Firebase project : $projectId" echo "Deployment URL : $deployedUrl" -if [[ $1 == "--dry-run" ]]; then +if [[ ${1:-} == "--dry-run" ]]; then exit 0 fi From e54bd59f228c73881c08b8f179f0fcf49e0f74ae Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Wed, 9 Aug 2017 23:11:51 +0200 Subject: [PATCH 04/83] fix(core): forbid destroyed views to be inserted or moved in VC (#18568) Fixes #17780 --- packages/core/src/view/refs.ts | 6 + packages/core/test/linker/integration_spec.ts | 304 ++++++++++-------- 2 files changed, 181 insertions(+), 129 deletions(-) diff --git a/packages/core/src/view/refs.ts b/packages/core/src/view/refs.ts index 9db6f7c44d..bb9040f601 100644 --- a/packages/core/src/view/refs.ts +++ b/packages/core/src/view/refs.ts @@ -187,6 +187,9 @@ class ViewContainerRef_ implements ViewContainerData { } insert(viewRef: ViewRef, index?: number): ViewRef { + if (viewRef.destroyed) { + throw new Error('Cannot insert a destroyed View in a ViewContainer!'); + } const viewRef_ = viewRef; const viewData = viewRef_._view; attachEmbeddedView(this._view, this._data, index, viewData); @@ -195,6 +198,9 @@ class ViewContainerRef_ implements ViewContainerData { } move(viewRef: ViewRef_, currentIndex: number): ViewRef { + if (viewRef.destroyed) { + throw new Error('Cannot move a destroyed View in a ViewContainer!'); + } const previousIndex = this._embeddedViews.indexOf(viewRef._view); moveEmbeddedView(this._data, previousIndex, currentIndex); return viewRef; diff --git a/packages/core/test/linker/integration_spec.ts b/packages/core/test/linker/integration_spec.ts index ce9ef724f8..f77fc7868e 100644 --- a/packages/core/test/linker/integration_spec.ts +++ b/packages/core/test/linker/integration_spec.ts @@ -7,7 +7,7 @@ */ import {CommonModule} from '@angular/common'; -import {Compiler, ComponentFactory, ErrorHandler, EventEmitter, Host, Inject, Injectable, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleRef, OnDestroy, SkipSelf} from '@angular/core'; +import {Compiler, ComponentFactory, ComponentRef, ErrorHandler, EventEmitter, Host, Inject, Injectable, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleRef, OnDestroy, SkipSelf, ViewRef} from '@angular/core'; import {ChangeDetectionStrategy, ChangeDetectorRef, PipeTransform} from '@angular/core/src/change_detection/change_detection'; import {getDebugContext} from '@angular/core/src/errors'; import {ComponentFactoryResolver} from '@angular/core/src/linker/component_factory_resolver'; @@ -1030,7 +1030,7 @@ function declareTests({useJit}: {useJit: boolean}) { fixture.destroy(); }); - describe('ViewContainerRef.createComponent', () => { + describe('ViewContainerRef', () => { beforeEach(() => { // we need a module to declarate ChildCompUsingService as an entryComponent otherwise the // factory doesn't get created @@ -1047,146 +1047,184 @@ function declareTests({useJit}: {useJit: boolean}) { MyComp, {add: {template: '
'}}); }); - it('should allow to create a component at any bound location', async(() => { - const fixture = TestBed.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}) - .createComponent(MyComp); - const tc = fixture.debugElement.children[0].children[0]; - const dynamicVp: DynamicViewport = tc.injector.get(DynamicViewport); - dynamicVp.create(); - fixture.detectChanges(); - expect(fixture.debugElement.children[0].children[1].nativeElement) - .toHaveText('dynamic greet'); - })); + describe('.createComponent', () => { + it('should allow to create a component at any bound location', async(() => { + const fixture = TestBed.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}) + .createComponent(MyComp); + const tc = fixture.debugElement.children[0].children[0]; + const dynamicVp: DynamicViewport = tc.injector.get(DynamicViewport); + dynamicVp.create(); + fixture.detectChanges(); + expect(fixture.debugElement.children[0].children[1].nativeElement) + .toHaveText('dynamic greet'); + })); - it('should allow to create multiple components at a location', async(() => { - const fixture = TestBed.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}) - .createComponent(MyComp); - const tc = fixture.debugElement.children[0].children[0]; - const dynamicVp: DynamicViewport = tc.injector.get(DynamicViewport); - dynamicVp.create(); - dynamicVp.create(); - fixture.detectChanges(); - expect(fixture.debugElement.children[0].children[1].nativeElement) - .toHaveText('dynamic greet'); - expect(fixture.debugElement.children[0].children[2].nativeElement) - .toHaveText('dynamic greet'); - })); + it('should allow to create multiple components at a location', async(() => { + const fixture = TestBed.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}) + .createComponent(MyComp); + const tc = fixture.debugElement.children[0].children[0]; + const dynamicVp: DynamicViewport = tc.injector.get(DynamicViewport); + dynamicVp.create(); + dynamicVp.create(); + fixture.detectChanges(); + expect(fixture.debugElement.children[0].children[1].nativeElement) + .toHaveText('dynamic greet'); + expect(fixture.debugElement.children[0].children[2].nativeElement) + .toHaveText('dynamic greet'); + })); - it('should create a component that has been freshly compiled', () => { - @Component({template: ''}) - class RootComp { - constructor(public vc: ViewContainerRef) {} - } + it('should create a component that has been freshly compiled', () => { + @Component({template: ''}) + class RootComp { + constructor(public vc: ViewContainerRef) {} + } - @NgModule({ - declarations: [RootComp], - providers: [{provide: 'someToken', useValue: 'someRootValue'}], - }) - class RootModule { - } + @NgModule({ + declarations: [RootComp], + providers: [{provide: 'someToken', useValue: 'someRootValue'}], + }) + class RootModule { + } - @Component({template: ''}) - class MyComp { - constructor(@Inject('someToken') public someToken: string) {} - } + @Component({template: ''}) + class MyComp { + constructor(@Inject('someToken') public someToken: string) {} + } - @NgModule({ - declarations: [MyComp], - providers: [{provide: 'someToken', useValue: 'someValue'}], - }) - class MyModule { - } + @NgModule({ + declarations: [MyComp], + providers: [{provide: 'someToken', useValue: 'someValue'}], + }) + class MyModule { + } - const compFixture = - TestBed.configureTestingModule({imports: [RootModule]}).createComponent(RootComp); - const compiler = TestBed.get(Compiler); - const myCompFactory = - >compiler.compileModuleAndAllComponentsSync(MyModule) - .componentFactories[0]; + const compFixture = + TestBed.configureTestingModule({imports: [RootModule]}).createComponent(RootComp); + const compiler = TestBed.get(Compiler); + const myCompFactory = + >compiler.compileModuleAndAllComponentsSync(MyModule) + .componentFactories[0]; - // Note: the ComponentFactory was created directly via the compiler, i.e. it - // does not have an association to an NgModuleRef. - // -> expect the providers of the module that the view container belongs to. - const compRef = compFixture.componentInstance.vc.createComponent(myCompFactory); - expect(compRef.instance.someToken).toBe('someRootValue'); + // Note: the ComponentFactory was created directly via the compiler, i.e. it + // does not have an association to an NgModuleRef. + // -> expect the providers of the module that the view container belongs to. + const compRef = compFixture.componentInstance.vc.createComponent(myCompFactory); + expect(compRef.instance.someToken).toBe('someRootValue'); + }); + + it('should create a component with the passed NgModuleRef', () => { + @Component({template: ''}) + class RootComp { + constructor(public vc: ViewContainerRef) {} + } + + @Component({template: ''}) + class MyComp { + constructor(@Inject('someToken') public someToken: string) {} + } + + @NgModule({ + declarations: [RootComp, MyComp], + entryComponents: [MyComp], + providers: [{provide: 'someToken', useValue: 'someRootValue'}], + }) + class RootModule { + } + + @NgModule({providers: [{provide: 'someToken', useValue: 'someValue'}]}) + class MyModule { + } + + const compFixture = + TestBed.configureTestingModule({imports: [RootModule]}).createComponent(RootComp); + const compiler = TestBed.get(Compiler); + const myModule = compiler.compileModuleSync(MyModule).create(TestBed.get(NgModuleRef)); + const myCompFactory = (TestBed.get(ComponentFactoryResolver)) + .resolveComponentFactory(MyComp); + + // Note: MyComp was declared as entryComponent in the RootModule, + // but we pass MyModule to the createComponent call. + // -> expect the providers of MyModule! + const compRef = compFixture.componentInstance.vc.createComponent( + myCompFactory, undefined, undefined, undefined, myModule); + expect(compRef.instance.someToken).toBe('someValue'); + }); + + it('should create a component with the NgModuleRef of the ComponentFactoryResolver', + () => { + @Component({template: ''}) + class RootComp { + constructor(public vc: ViewContainerRef) {} + } + + @NgModule({ + declarations: [RootComp], + providers: [{provide: 'someToken', useValue: 'someRootValue'}], + }) + class RootModule { + } + + @Component({template: ''}) + class MyComp { + constructor(@Inject('someToken') public someToken: string) {} + } + + @NgModule({ + declarations: [MyComp], + entryComponents: [MyComp], + providers: [{provide: 'someToken', useValue: 'someValue'}], + }) + class MyModule { + } + + const compFixture = TestBed.configureTestingModule({imports: [RootModule]}) + .createComponent(RootComp); + const compiler = TestBed.get(Compiler); + const myModule = + compiler.compileModuleSync(MyModule).create(TestBed.get(NgModuleRef)); + const myCompFactory = + myModule.componentFactoryResolver.resolveComponentFactory(MyComp); + + // Note: MyComp was declared as entryComponent in MyModule, + // and we don't pass an explicit ModuleRef to the createComponent call. + // -> expect the providers of MyModule! + const compRef = compFixture.componentInstance.vc.createComponent(myCompFactory); + expect(compRef.instance.someToken).toBe('someValue'); + }); }); - it('should create a component with the passed NgModuleRef', () => { - @Component({template: ''}) - class RootComp { - constructor(public vc: ViewContainerRef) {} - } + describe('.insert', () => { + it('should throw with destroyed views', async(() => { + const fixture = TestBed.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}) + .createComponent(MyComp); + const tc = fixture.debugElement.children[0].children[0]; + const dynamicVp: DynamicViewport = tc.injector.get(DynamicViewport); + const ref = dynamicVp.create(); + fixture.detectChanges(); - @Component({template: ''}) - class MyComp { - constructor(@Inject('someToken') public someToken: string) {} - } - - @NgModule({ - declarations: [RootComp, MyComp], - entryComponents: [MyComp], - providers: [{provide: 'someToken', useValue: 'someRootValue'}], - }) - class RootModule { - } - - @NgModule({providers: [{provide: 'someToken', useValue: 'someValue'}]}) - class MyModule { - } - - const compFixture = - TestBed.configureTestingModule({imports: [RootModule]}).createComponent(RootComp); - const compiler = TestBed.get(Compiler); - const myModule = compiler.compileModuleSync(MyModule).create(TestBed.get(NgModuleRef)); - const myCompFactory = (TestBed.get(ComponentFactoryResolver)) - .resolveComponentFactory(MyComp); - - // Note: MyComp was declared as entryComponent in the RootModule, - // but we pass MyModule to the createComponent call. - // -> expect the providers of MyModule! - const compRef = compFixture.componentInstance.vc.createComponent( - myCompFactory, undefined, undefined, undefined, myModule); - expect(compRef.instance.someToken).toBe('someValue'); + ref.destroy(); + expect(() => { + dynamicVp.insert(ref.hostView); + }).toThrowError('Cannot insert a destroyed View in a ViewContainer!'); + })); }); - it('should create a component with the NgModuleRef of the ComponentFactoryResolver', () => { - @Component({template: ''}) - class RootComp { - constructor(public vc: ViewContainerRef) {} - } + describe('.move', () => { + it('should throw with destroyed views', async(() => { + const fixture = TestBed.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}) + .createComponent(MyComp); + const tc = fixture.debugElement.children[0].children[0]; + const dynamicVp: DynamicViewport = tc.injector.get(DynamicViewport); + const ref = dynamicVp.create(); + fixture.detectChanges(); - @NgModule({ - declarations: [RootComp], - providers: [{provide: 'someToken', useValue: 'someRootValue'}], - }) - class RootModule { - } - - @Component({template: ''}) - class MyComp { - constructor(@Inject('someToken') public someToken: string) {} - } - - @NgModule({ - declarations: [MyComp], - entryComponents: [MyComp], - providers: [{provide: 'someToken', useValue: 'someValue'}], - }) - class MyModule { - } - - const compFixture = - TestBed.configureTestingModule({imports: [RootModule]}).createComponent(RootComp); - const compiler = TestBed.get(Compiler); - const myModule = compiler.compileModuleSync(MyModule).create(TestBed.get(NgModuleRef)); - const myCompFactory = myModule.componentFactoryResolver.resolveComponentFactory(MyComp); - - // Note: MyComp was declared as entryComponent in MyModule, - // and we don't pass an explicit ModuleRef to the createComponent call. - // -> expect the providers of MyModule! - const compRef = compFixture.componentInstance.vc.createComponent(myCompFactory); - expect(compRef.instance.someToken).toBe('someValue'); + ref.destroy(); + expect(() => { + dynamicVp.move(ref.hostView, 1); + }).toThrowError('Cannot move a destroyed View in a ViewContainer!'); + })); }); + }); it('should support static attributes', () => { @@ -1855,7 +1893,15 @@ class DynamicViewport { componentFactoryResolver.resolveComponentFactory(ChildCompUsingService) !; } - create() { this.vc.createComponent(this.componentFactory, this.vc.length, this.injector); } + create(): ComponentRef { + return this.vc.createComponent(this.componentFactory, this.vc.length, this.injector); + } + + insert(viewRef: ViewRef, index?: number): ViewRef { return this.vc.insert(viewRef, index); } + + move(viewRef: ViewRef, currentIndex: number): ViewRef { + return this.vc.move(viewRef, currentIndex); + } } @Directive({selector: '[my-dir]', inputs: ['dirProp: elprop'], exportAs: 'mydir'}) From 2f9d8ff46d8d66cedd335ca3b17cc3b40a49f9b7 Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Wed, 9 Aug 2017 23:15:40 +0200 Subject: [PATCH 05/83] test(animations): disable buggy test in Chrome 39 (#18483) Fixes #15793 --- ...ns_with_web_animations_integration_spec.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/core/test/animation/animations_with_web_animations_integration_spec.ts b/packages/core/test/animation/animations_with_web_animations_integration_spec.ts index 4664815a95..e257209fe0 100644 --- a/packages/core/test/animation/animations_with_web_animations_integration_spec.ts +++ b/packages/core/test/animation/animations_with_web_animations_integration_spec.ts @@ -10,11 +10,13 @@ import {AnimationDriver, ɵAnimationEngine, ɵWebAnimationsDriver, ɵWebAnimatio import {AnimationGroupPlayer} from '@angular/animations/src/players/animation_group_player'; import {Component} from '@angular/core'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {browserDetection} from '@angular/platform-browser/testing/src/browser_util'; import {TestBed} from '../../testing'; export function main() { // these tests are only mean't to be run within the DOM (for now) + // Buggy in Chromium 39, see https://github.com/angular/angular/issues/15793 if (typeof Element == 'undefined' || !ɵsupportsWebAnimations()) return; describe('animation integration tests using web animations', function() { @@ -66,16 +68,18 @@ export function main() { {height: '0px', offset: 0}, {height: '100px', offset: 1} ]); - cmp.exp = false; - fixture.detectChanges(); - engine.flush(); + if (!browserDetection.isOldChrome) { + cmp.exp = false; + fixture.detectChanges(); + engine.flush(); - expect(engine.players.length).toEqual(1); - webPlayer = engine.players[0].getRealPlayer() as ɵWebAnimationsPlayer; + expect(engine.players.length).toEqual(1); + webPlayer = engine.players[0].getRealPlayer() as ɵWebAnimationsPlayer; - expect(webPlayer.keyframes).toEqual([ - {height: '100px', offset: 0}, {height: '0px', offset: 1} - ]); + expect(webPlayer.keyframes).toEqual([ + {height: '100px', offset: 0}, {height: '0px', offset: 1} + ]); + } }); it('should compute (!) animation styles for a container that is being inserted', () => { @@ -293,4 +297,4 @@ export function main() { function approximate(value: number, target: number) { return Math.abs(target - value) / value; -} \ No newline at end of file +} From 6fb5250185d9e67409947d08f81c4f1f8a11e85a Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Wed, 9 Aug 2017 02:18:31 +0300 Subject: [PATCH 06/83] ci(aio): fix deploying the stable branch to Firebase The `deploy-to-firebase.sh` always expects there to be a `src/extra-files/` directory and breaks if it doesn't exist. --- aio/src/extra-files/README.md | 6 +++++- aio/src/extra-files/archive/robots.txt | 3 ++- aio/src/extra-files/next/robots.txt | 3 ++- aio/src/extra-files/stable/robots.txt | 3 +++ 4 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 aio/src/extra-files/stable/robots.txt diff --git a/aio/src/extra-files/README.md b/aio/src/extra-files/README.md index 2aad208a30..55a09439bf 100644 --- a/aio/src/extra-files/README.md +++ b/aio/src/extra-files/README.md @@ -6,4 +6,8 @@ After the AIO application had been built and before it is deployed all files and 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 +See the `scripts/deploy-to-firebase.sh` script for more detail. + +**Note:** +The `deploy-to-firebase.sh` script always expects there to be a folder for the current deployment +mode (even if it is empty). diff --git a/aio/src/extra-files/archive/robots.txt b/aio/src/extra-files/archive/robots.txt index 77470cb39f..35272cac72 100644 --- a/aio/src/extra-files/archive/robots.txt +++ b/aio/src/extra-files/archive/robots.txt @@ -1,2 +1,3 @@ +# Disallow all URLs (see http://www.robotstxt.org/robotstxt.html) User-agent: * -Disallow: / \ No newline at end of file +Disallow: / diff --git a/aio/src/extra-files/next/robots.txt b/aio/src/extra-files/next/robots.txt index 77470cb39f..35272cac72 100644 --- a/aio/src/extra-files/next/robots.txt +++ b/aio/src/extra-files/next/robots.txt @@ -1,2 +1,3 @@ +# Disallow all URLs (see http://www.robotstxt.org/robotstxt.html) User-agent: * -Disallow: / \ No newline at end of file +Disallow: / diff --git a/aio/src/extra-files/stable/robots.txt b/aio/src/extra-files/stable/robots.txt new file mode 100644 index 0000000000..7dbb786bc6 --- /dev/null +++ b/aio/src/extra-files/stable/robots.txt @@ -0,0 +1,3 @@ +# Allow all URLs (see http://www.robotstxt.org/robotstxt.html) +User-agent: * +Disallow: From b14250bef951dc6125d527d331f857cd6bc9fdad Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Wed, 9 Aug 2017 02:22:12 +0300 Subject: [PATCH 07/83] test(aio): fix the `deploy-to-firebase` tests This commit also ensures that if the tests fail, the script exits with an error. --- aio/scripts/deploy-to-firebase.test.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aio/scripts/deploy-to-firebase.test.sh b/aio/scripts/deploy-to-firebase.test.sh index dd9a3235fd..6093a3ab0e 100755 --- a/aio/scripts/deploy-to-firebase.test.sh +++ b/aio/scripts/deploy-to-firebase.test.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +set +x -eu -o pipefail function check { if [[ $1 == $2 ]]; then @@ -133,11 +134,11 @@ We only deploy archive branches with the major version less than the stable bran 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 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="Skipping deploy of branch \"2.1.x\" to firebase. + expected="Skipping deploy of branch \"2.4.x\" to firebase. We only deploy archive branches with the major version less than the stable branch: \"2.2.x\"" check "$actual" "$expected" ) From fd6ae571b8180835ba936f6125f184057e9ab498 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Thu, 10 Aug 2017 00:20:25 +0300 Subject: [PATCH 08/83] fix(aio): add missing code snippet (#18547) The snippet got lost some time during the migration from the old version (it is [present in v2][1]). [1]: https://v2.angular.io/docs/ts/latest/cookbook/aot-compiler.html#!#running-the-application Fixes #18544 --- aio/content/guide/aot-compiler.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/aio/content/guide/aot-compiler.md b/aio/content/guide/aot-compiler.md index b48baf59b6..a5ddc8315f 100644 --- a/aio/content/guide/aot-compiler.md +++ b/aio/content/guide/aot-compiler.md @@ -528,6 +528,11 @@ Compiling with AOT presupposes certain supporting files, most of them discussed Extend the `scripts` section of the `package.json` with these npm scripts: + + "build:aot": "ngc -p tsconfig-aot.json && rollup -c rollup-config.js", + "serve:aot": "lite-server -c bs-config.aot.json", + + Copy the AOT distribution files into the `/aot` folder with the node script: From 7f2037f0b6f1aec0a79a6586fefd7df85e370c83 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Thu, 10 Aug 2017 00:21:10 +0300 Subject: [PATCH 09/83] test(aio): fix running docs examples against local builds (#18520) This commit also updates the version of `@angular/cli` used for docs examples. The previous (transient) dependency `@ngtools/webpack` was not compatible with `@angular/compiler-cli@>=5` and was breaking when running against the local builds (currently at 5.0.0-beta.2). The version of `@ngtools/webpack` used by the latest `@angular/cli` version is compatible with `@angular/compiler-cli@5`. --- aio/tools/examples/example-boilerplate.js | 8 +- .../examples/example-boilerplate.spec.js | 9 +- aio/tools/examples/run-example-e2e.js | 2 +- aio/tools/examples/shared/package.json | 2 +- aio/tools/examples/shared/yarn.lock | 131 +++++++++++++----- scripts/ci/build.sh | 10 ++ scripts/ci/test-aio.sh | 8 ++ 7 files changed, 123 insertions(+), 47 deletions(-) diff --git a/aio/tools/examples/example-boilerplate.js b/aio/tools/examples/example-boilerplate.js index 7127f1c968..bc8944025a 100644 --- a/aio/tools/examples/example-boilerplate.js +++ b/aio/tools/examples/example-boilerplate.js @@ -41,12 +41,9 @@ const ANGULAR_PACKAGES = [ 'platform-browser-dynamic', 'platform-server', 'router', + 'tsc-wrapped', 'upgrade', ]; -const ANGULAR_TOOLS_PACKAGES_PATH = path.resolve(ANGULAR_DIST_PATH, 'tools', '@angular'); -const ANGULAR_TOOLS_PACKAGES = [ - 'tsc-wrapped' -]; const EXAMPLE_CONFIG_FILENAME = 'example-config.json'; @@ -63,7 +60,6 @@ class ExampleBoilerPlate { // Replace the Angular packages with those from the dist folder, if necessary if (useLocal) { ANGULAR_PACKAGES.forEach(packageName => this.overridePackage(ANGULAR_PACKAGES_PATH, packageName)); - ANGULAR_TOOLS_PACKAGES.forEach(packageName => this.overridePackage(ANGULAR_TOOLS_PACKAGES_PATH, packageName)); } // Get all the examples folders, indicated by those that contain a `example-config.json` file @@ -111,7 +107,7 @@ class ExampleBoilerPlate { const sourceFolder = path.resolve(basePath, packageName); const destinationFolder = path.resolve(SHARED_NODE_MODULES_PATH, '@angular', packageName); shelljs.rm('-rf', destinationFolder); - fs.ensureSymlinkSync(sourceFolder, destinationFolder); + fs.copySync(sourceFolder, destinationFolder); } getFoldersContaining(basePath, filename, ignore) { diff --git a/aio/tools/examples/example-boilerplate.spec.js b/aio/tools/examples/example-boilerplate.spec.js index e5d075afd4..b983e86558 100644 --- a/aio/tools/examples/example-boilerplate.spec.js +++ b/aio/tools/examples/example-boilerplate.spec.js @@ -30,8 +30,9 @@ describe('example-boilerplate tool', () => { exampleBoilerPlate.add(true); expect(exampleBoilerPlate.overridePackage).toHaveBeenCalledTimes(numberOfAngularPackages + numberOfAngularToolsPackages); // for example + expect(exampleBoilerPlate.overridePackage).toHaveBeenCalledWith(path.resolve(__dirname, '../../../dist/packages-dist'), 'common'); expect(exampleBoilerPlate.overridePackage).toHaveBeenCalledWith(path.resolve(__dirname, '../../../dist/packages-dist'), 'core'); - expect(exampleBoilerPlate.overridePackage).toHaveBeenCalledWith(path.resolve(__dirname, '../../../dist/tools/@angular'), 'tsc-wrapped'); + expect(exampleBoilerPlate.overridePackage).toHaveBeenCalledWith(path.resolve(__dirname, '../../../dist/packages-dist'), 'tsc-wrapped'); }); it('should process all the example folders', () => { @@ -90,7 +91,7 @@ describe('example-boilerplate tool', () => { describe('overridePackage', () => { beforeEach(() => { spyOn(shelljs, 'rm'); - spyOn(fs, 'ensureSymlinkSync'); + spyOn(fs, 'copySync'); }); it('should remove the original package from the shared node_modules folder', () => { @@ -98,9 +99,9 @@ describe('example-boilerplate tool', () => { expect(shelljs.rm).toHaveBeenCalledWith('-rf', path.resolve(__dirname, 'shared/node_modules/@angular/somePackage')); }); - it('should symlink the source folder to the shared node_modules folder', () => { + it('should copy the source folder to the shared node_modules folder', () => { exampleBoilerPlate.overridePackage('base/path', 'somePackage'); - expect(fs.ensureSymlinkSync).toHaveBeenCalledWith(path.resolve('base/path/somePackage'), path.resolve(__dirname, 'shared/node_modules/@angular/somePackage')); + expect(fs.copySync).toHaveBeenCalledWith(path.resolve('base/path/somePackage'), path.resolve(__dirname, 'shared/node_modules/@angular/somePackage')); }); }); diff --git a/aio/tools/examples/run-example-e2e.js b/aio/tools/examples/run-example-e2e.js index 0b4e978592..48ceb9cc97 100644 --- a/aio/tools/examples/run-example-e2e.js +++ b/aio/tools/examples/run-example-e2e.js @@ -39,7 +39,7 @@ function runE2e() { if (argv.setup) { // Run setup. console.log('runE2e: copy boilerplate'); - const spawnInfo = spawnExt('yarn', ['boilerplate:add', argv.local ? '-- --local': ''], { cwd: AIO_PATH }); + const spawnInfo = spawnExt('yarn', ['boilerplate:add', '--', argv.local ? '--local' : ''], { cwd: AIO_PATH }); promise = spawnInfo.promise .then(() => { console.log('runE2e: update webdriver'); diff --git a/aio/tools/examples/shared/package.json b/aio/tools/examples/shared/package.json index 4627b75488..47c5ea6d37 100644 --- a/aio/tools/examples/shared/package.json +++ b/aio/tools/examples/shared/package.json @@ -32,7 +32,7 @@ "zone.js": "^0.8.4" }, "devDependencies": { - "@angular/cli": "^1.2.0", + "@angular/cli": "^1.2.7", "@types/angular": "^1.5.16", "@types/angular-animate": "^1.5.5", "@types/angular-cookies": "^1.4.2", diff --git a/aio/tools/examples/shared/yarn.lock b/aio/tools/examples/shared/yarn.lock index 2fd742b899..03d8325b9d 100644 --- a/aio/tools/examples/shared/yarn.lock +++ b/aio/tools/examples/shared/yarn.lock @@ -1,7 +1,5 @@ # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. # yarn lockfile v1 -# yarn v0.25.3 -# node v7.8.0 "@angular/animations@~4.3.1": @@ -10,14 +8,15 @@ dependencies: tslib "^1.7.1" -"@angular/cli@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-1.2.0.tgz#dfd8b8983ec37c2b6d7f902ead60396d7b571597" +"@angular/cli@^1.2.7": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-1.2.7.tgz#dd20e8b0da24af5359077c05a6944823cb764132" dependencies: "@ngtools/json-schema" "1.1.0" - "@ngtools/webpack" "1.5.0" + "@ngtools/webpack" "1.5.5" 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" @@ -29,7 +28,7 @@ exports-loader "^0.6.3" extract-text-webpack-plugin "^2.1.0" file-loader "^0.10.0" - fs-extra "^3.0.1" + fs-extra "^4.0.0" get-caller-file "^1.0.0" glob "^7.0.3" heimdalljs "^0.2.4" @@ -39,7 +38,7 @@ 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" license-webpack-plugin "^0.4.2" @@ -48,7 +47,7 @@ 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" @@ -61,6 +60,7 @@ semver "^5.1.0" silent-error "^1.0.0" source-map-loader "^0.2.0" + source-map-support "^0.4.1" style-loader "^0.13.1" stylus "^0.54.5" stylus-loader "^3.0.1" @@ -72,7 +72,7 @@ webpack-dev-middleware "^1.10.2" webpack-dev-server "~2.4.5" webpack-merge "^2.4.0" - zone.js "^0.8.4" + zone.js "^0.8.14" optionalDependencies: node-sass "^4.3.0" @@ -156,13 +156,12 @@ version "1.1.0" resolved "https://registry.yarnpkg.com/@ngtools/json-schema/-/json-schema-1.1.0.tgz#c3a0c544d62392acc2813a42c8a0dc6f58f86922" -"@ngtools/webpack@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-1.5.0.tgz#b6be58d857d41f8999751d6bbc3d21e84bc977ca" +"@ngtools/webpack@1.5.5": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-1.5.5.tgz#d6e2a933184015653de3be33d38437fdd81009e7" 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/angular-animate@^1.5.5": @@ -360,6 +359,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" + anymatch@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507" @@ -1385,6 +1390,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" + chokidar@1.7.0, chokidar@^1.4.1, chokidar@^1.4.3, chokidar@^1.6.0, chokidar@^1.6.1: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" @@ -1406,6 +1419,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" + clap@^1.0.9: version "1.1.3" resolved "https://registry.yarnpkg.com/clap/-/clap-1.1.3.tgz#b3bd36e93dd4cbfb395a3c26896352445265c05b" @@ -1476,7 +1493,7 @@ code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" -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: @@ -2592,7 +2609,7 @@ fs-access@^1.0.0: dependencies: null-check "^1.0.0" -fs-extra@3.0.1, fs-extra@^3.0.1: +fs-extra@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291" dependencies: @@ -2609,6 +2626,14 @@ fs-extra@^0.23.1: path-is-absolute "^1.0.0" rimraf "^2.2.8" +fs-extra@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.1.tgz#7fc0c6c8957f983f57f306a24e5b9ddd8d0dd880" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^3.0.0" + universalify "^0.1.0" + fs-extra@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" @@ -2843,6 +2868,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" @@ -3340,6 +3369,10 @@ is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" +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" @@ -3539,6 +3572,12 @@ karma-phantomjs-launcher@^1.0.2: lodash "^4.0.1" phantomjs-prebuilt "^2.1.7" +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-sourcemap-loader@^0.3.7: version "0.3.7" resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz#91322c77f8f13d46fed062b042e1009d4c4505d8" @@ -3803,6 +3842,12 @@ magic-string@^0.19.0: 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-error@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.0.tgz#52ad3a339ccf10ce62b4040b708fe707244b8b96" @@ -4241,6 +4286,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: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" @@ -5112,18 +5163,18 @@ request-progress@~2.0.1: dependencies: throttleit "^1.0.0" -request@2, request@^2.72.0, request@^2.78.0, request@^2.79.0, request@~2.79.0: - version "2.79.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" +request@2, request@^2.72.0, request@^2.79.0, request@^2.81.0: + version "2.81.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" dependencies: aws-sign2 "~0.6.0" aws4 "^1.2.1" - caseless "~0.11.0" + caseless "~0.12.0" combined-stream "~1.0.5" extend "~3.0.0" forever-agent "~0.6.1" form-data "~2.1.1" - har-validator "~2.0.6" + har-validator "~4.2.1" hawk "~3.1.3" http-signature "~1.1.0" is-typedarray "~1.0.0" @@ -5131,10 +5182,12 @@ request@2, request@^2.72.0, request@^2.78.0, request@^2.79.0, request@~2.79.0: json-stringify-safe "~5.0.1" mime-types "~2.1.7" oauth-sign "~0.8.1" - qs "~6.3.0" + performance-now "^0.2.0" + qs "~6.4.0" + safe-buffer "^5.0.1" stringstream "~0.0.4" tough-cookie "~2.3.0" - tunnel-agent "~0.4.1" + tunnel-agent "^0.6.0" uuid "^3.0.0" request@2.78.0: @@ -5162,18 +5215,18 @@ request@2.78.0: tough-cookie "~2.3.0" tunnel-agent "~0.4.1" -request@^2.81.0: - version "2.81.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" +request@^2.78.0, request@~2.79.0: + version "2.79.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" dependencies: aws-sign2 "~0.6.0" aws4 "^1.2.1" - caseless "~0.12.0" + caseless "~0.11.0" combined-stream "~1.0.5" extend "~3.0.0" forever-agent "~0.6.1" form-data "~2.1.1" - har-validator "~4.2.1" + har-validator "~2.0.6" hawk "~3.1.3" http-signature "~1.1.0" is-typedarray "~1.0.0" @@ -5181,12 +5234,10 @@ request@^2.81.0: json-stringify-safe "~5.0.1" mime-types "~2.1.7" oauth-sign "~0.8.1" - performance-now "^0.2.0" - qs "~6.4.0" - safe-buffer "^5.0.1" + qs "~6.3.0" stringstream "~0.0.4" tough-cookie "~2.3.0" - tunnel-agent "^0.6.0" + tunnel-agent "~0.4.1" uuid "^3.0.0" require-directory@^2.1.1: @@ -5696,7 +5747,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: @@ -5919,6 +5970,12 @@ supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.2.3: dependencies: has-flag "^1.0.0" +supports-color@^4.0.0: + 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" @@ -6750,6 +6807,10 @@ yn@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" +zone.js@^0.8.14: + version "0.8.16" + resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.16.tgz#ac31b6c418f88c0f918ad6acd8a402aca9313abb" + zone.js@^0.8.4: version "0.8.12" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.12.tgz#86ff5053c98aec291a0bf4bbac501d694a05cfbb" diff --git a/scripts/ci/build.sh b/scripts/ci/build.sh index 891d36cdef..700a261264 100755 --- a/scripts/ci/build.sh +++ b/scripts/ci/build.sh @@ -40,6 +40,16 @@ if [[ ${CI_MODE:-} == "aio" ]]; then exit 0; fi +# Build the Angular packages then exit (no further build required) +if [[ ${CI_MODE:-} == "aio_e2e" ]]; then + travisFoldStart "build.aio_e2e" + ( + ./build.sh + ) + travisFoldEnd "build.aio_e2e" + exit 0; +fi + travisFoldStart "tsc tools" $(npm bin)/tsc -p tools $(npm bin)/tsc -p packages/tsc-wrapped/tsconfig-build.json diff --git a/scripts/ci/test-aio.sh b/scripts/ci/test-aio.sh index cd2bcfaf85..12374312f5 100755 --- a/scripts/ci/test-aio.sh +++ b/scripts/ci/test-aio.sh @@ -18,6 +18,12 @@ source ${thisDir}/_travis-fold.sh travisFoldEnd "test.aio.lint" + # Run unit tests for boilerplate tools + travisFoldStart "test.aio.boilerplate.unit" + yarn boilerplate:test + travisFoldEnd "test.aio.boilerplate.unit" + + # Run unit tests travisFoldStart "test.aio.unit" yarn test -- --single-run @@ -29,11 +35,13 @@ source ${thisDir}/_travis-fold.sh yarn e2e travisFoldEnd "test.aio.e2e" + # Run PWA-score tests travisFoldStart "test.aio.pwaScore" yarn test-pwa-score-local travisFoldEnd "test.aio.pwaScore" + # Run unit tests for aio/aio-builds-setup travisFoldStart "test.aio.aio-builds-setup" ./aio-builds-setup/scripts/test.sh From dca50deae473aa1d59b7d57a4014b75d67a84053 Mon Sep 17 00:00:00 2001 From: Trotyl Date: Wed, 9 Aug 2017 15:18:05 +0800 Subject: [PATCH 10/83] docs(core): deprecate ReflectiveInjector closes #18598 --- packages/core/src/di/reflective_injector.ts | 4 +--- tools/public_api_guard/core/core.d.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/core/src/di/reflective_injector.ts b/packages/core/src/di/reflective_injector.ts index 2dd381b657..5e7e0a481c 100644 --- a/packages/core/src/di/reflective_injector.ts +++ b/packages/core/src/di/reflective_injector.ts @@ -13,8 +13,6 @@ import {cyclicDependencyError, instantiationError, noProviderError, outOfBoundsE import {ReflectiveKey} from './reflective_key'; import {ReflectiveDependency, ResolvedReflectiveFactory, ResolvedReflectiveProvider, resolveReflectiveProviders} from './reflective_provider'; - - // Threshold for the dynamic version const UNDEFINED = new Object(); @@ -51,7 +49,7 @@ const UNDEFINED = new Object(); * Notice, we don't use the `new` operator because we explicitly want to have the `Injector` * resolve all of the object's dependencies automatically. * - * @stable + * @deprecated from v5 - slow and brings in a lot of code, Use `Injector.create` instead. */ export abstract class ReflectiveInjector implements Injector { /** diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 2487babe7c..936b0a0a58 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -759,7 +759,7 @@ export declare class QueryList { toString(): string; } -/** @stable */ +/** @deprecated */ export declare abstract class ReflectiveInjector implements Injector { readonly abstract parent: Injector | null; abstract createChildFromResolved(providers: ResolvedReflectiveProvider[]): ReflectiveInjector; From ff5c58be6b843bc8f79613dbefc5e4951c125f3d Mon Sep 17 00:00:00 2001 From: Kara Date: Wed, 9 Aug 2017 15:41:53 -0700 Subject: [PATCH 11/83] feat(forms): add default updateOn values for groups and arrays (#18536) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds support for setting default `updateOn` values in `FormGroups` and `FormArrays`. If you set `updateOn` to ’blur’` at the group level, all child controls will default to `’blur’`, unless the child has explicitly specified a different `updateOn` value. ``` const c = new FormGroup({ one: new FormControl() }, {updateOn: blur}); ``` It's worth noting that parent groups will always update their value and validity immediately upon value/validity updates from children. In other words, if a group is set to update on blur and its children are individually set to update on change, the group will still update on change with its children; its default value will simply not be used. --- .../form_group_directive.ts | 2 +- packages/forms/src/directives/shared.ts | 6 +- packages/forms/src/model.ts | 54 ++++- packages/forms/test/form_control_spec.ts | 73 ++++++- .../forms/test/reactive_integration_spec.ts | 187 +++++++++++++++++- tools/public_api_guard/forms/forms.d.ts | 1 + 6 files changed, 302 insertions(+), 21 deletions(-) diff --git a/packages/forms/src/directives/reactive_directives/form_group_directive.ts b/packages/forms/src/directives/reactive_directives/form_group_directive.ts index 15d08eeab3..8598d083ae 100644 --- a/packages/forms/src/directives/reactive_directives/form_group_directive.ts +++ b/packages/forms/src/directives/reactive_directives/form_group_directive.ts @@ -150,7 +150,7 @@ export class FormGroupDirective extends ControlContainer implements Form, _syncPendingControls() { this.form._syncPendingControls(); this.directives.forEach(dir => { - if (dir.control._updateOn === 'submit') { + if (dir.control.updateOn === 'submit') { dir.viewToModelUpdate(dir.control._pendingValue); } }); diff --git a/packages/forms/src/directives/shared.ts b/packages/forms/src/directives/shared.ts index 428f502743..50651fe55e 100644 --- a/packages/forms/src/directives/shared.ts +++ b/packages/forms/src/directives/shared.ts @@ -84,7 +84,7 @@ function setUpViewChangePipeline(control: FormControl, dir: NgControl): void { control._pendingValue = newValue; control._pendingDirty = true; - if (control._updateOn === 'change') updateControl(control, dir); + if (control.updateOn === 'change') updateControl(control, dir); }); } @@ -92,8 +92,8 @@ function setUpBlurPipeline(control: FormControl, dir: NgControl): void { dir.valueAccessor !.registerOnTouched(() => { control._pendingTouched = true; - if (control._updateOn === 'blur') updateControl(control, dir); - if (control._updateOn !== 'submit') control.markAsTouched(); + if (control.updateOn === 'blur') updateControl(control, dir); + if (control.updateOn !== 'submit') control.markAsTouched(); }); } diff --git a/packages/forms/src/model.ts b/packages/forms/src/model.ts index b0d1e64058..acec83c417 100644 --- a/packages/forms/src/model.ts +++ b/packages/forms/src/model.ts @@ -118,6 +118,9 @@ export abstract class AbstractControl { /** @internal */ _onCollectionChange = () => {}; + /** @internal */ + _updateOn: FormHooks; + private _valueChanges: EventEmitter; private _statusChanges: EventEmitter; private _status: string; @@ -242,6 +245,15 @@ export abstract class AbstractControl { */ get statusChanges(): Observable { return this._statusChanges; } + /** + * Returns the update strategy of the `AbstractControl` (i.e. + * the event on which the control will update itself). + * Possible values: `'change'` (default) | `'blur'` | `'submit'` + */ + get updateOn(): FormHooks { + return this._updateOn ? this._updateOn : (this.parent ? this.parent.updateOn : 'change'); + } + /** * Sets the synchronous validators that are active on this control. Calling * this will overwrite any existing sync validators. @@ -624,6 +636,13 @@ export abstract class AbstractControl { /** @internal */ _registerOnCollectionChange(fn: () => void): void { this._onCollectionChange = fn; } + + /** @internal */ + _setUpdateStrategy(opts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null): void { + if (isOptionsObj(opts) && (opts as AbstractControlOptions).updateOn != null) { + this._updateOn = (opts as AbstractControlOptions).updateOn !; + } + } } /** @@ -697,9 +716,6 @@ export class FormControl extends AbstractControl { /** @internal */ _onChange: Function[] = []; - /** @internal */ - _updateOn: FormHooks = 'change'; - /** @internal */ _pendingValue: any; @@ -841,7 +857,7 @@ export class FormControl extends AbstractControl { /** @internal */ _syncPendingControls(): boolean { - if (this._updateOn === 'submit') { + if (this.updateOn === 'submit') { this.setValue(this._pendingValue, {onlySelf: true, emitModelToViewChange: false}); if (this._pendingDirty) this.markAsDirty(); if (this._pendingTouched) this.markAsTouched(); @@ -859,12 +875,6 @@ export class FormControl extends AbstractControl { this._value = this._pendingValue = formState; } } - - private _setUpdateStrategy(opts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null): void { - if (isOptionsObj(opts) && (opts as AbstractControlOptions).updateOn != null) { - this._updateOn = (opts as AbstractControlOptions).updateOn !; - } - } } /** @@ -925,6 +935,17 @@ export class FormControl extends AbstractControl { * }, {validators: passwordMatchValidator, asyncValidators: otherValidator}); * ``` * + * The options object can also be used to set a default value for each child + * control's `updateOn` property. If you set `updateOn` to `'blur'` at the + * group level, all child controls will default to 'blur', unless the child + * has explicitly specified a different `updateOn` value. + * + * ```ts + * const c = new FormGroup({ + * one: new FormControl() + * }, {updateOn: 'blur'}); + * ``` + * * * **npm package**: `@angular/forms` * * @stable @@ -938,6 +959,7 @@ export class FormGroup extends AbstractControl { coerceToValidator(validatorOrOpts), coerceToAsyncValidator(asyncValidator, validatorOrOpts)); this._initObservables(); + this._setUpdateStrategy(validatorOrOpts); this._setUpControls(); this.updateValueAndValidity({onlySelf: true, emitEvent: false}); } @@ -1242,6 +1264,17 @@ export class FormGroup extends AbstractControl { * ], {validators: myValidator, asyncValidators: myAsyncValidator}); * ``` * + * The options object can also be used to set a default value for each child + * control's `updateOn` property. If you set `updateOn` to `'blur'` at the + * array level, all child controls will default to 'blur', unless the child + * has explicitly specified a different `updateOn` value. + * + * ```ts + * const c = new FormArray([ + * new FormControl() + * ], {updateOn: 'blur'}); + * ``` + * * ### Adding or removing controls * * To change the controls in the array, use the `push`, `insert`, or `removeAt` methods @@ -1263,6 +1296,7 @@ export class FormArray extends AbstractControl { coerceToValidator(validatorOrOpts), coerceToAsyncValidator(asyncValidator, validatorOrOpts)); this._initObservables(); + this._setUpdateStrategy(validatorOrOpts); this._setUpControls(); this.updateValueAndValidity({onlySelf: true, emitEvent: false}); } diff --git a/packages/forms/test/form_control_spec.ts b/packages/forms/test/form_control_spec.ts index a424a5619a..cd3d1fe07b 100644 --- a/packages/forms/test/form_control_spec.ts +++ b/packages/forms/test/form_control_spec.ts @@ -80,17 +80,84 @@ export function main() { it('should default to on change', () => { const c = new FormControl(''); - expect(c._updateOn).toEqual('change'); + expect(c.updateOn).toEqual('change'); }); it('should default to on change with an options obj', () => { const c = new FormControl('', {validators: Validators.required}); - expect(c._updateOn).toEqual('change'); + expect(c.updateOn).toEqual('change'); }); it('should set updateOn when updating on blur', () => { const c = new FormControl('', {updateOn: 'blur'}); - expect(c._updateOn).toEqual('blur'); + expect(c.updateOn).toEqual('blur'); + }); + + describe('in groups and arrays', () => { + it('should default to group updateOn when not set in control', () => { + const g = + new FormGroup({one: new FormControl(), two: new FormControl()}, {updateOn: 'blur'}); + + expect(g.get('one') !.updateOn).toEqual('blur'); + expect(g.get('two') !.updateOn).toEqual('blur'); + }); + + it('should default to array updateOn when not set in control', () => { + const a = new FormArray([new FormControl(), new FormControl()], {updateOn: 'blur'}); + + expect(a.get([0]) !.updateOn).toEqual('blur'); + expect(a.get([1]) !.updateOn).toEqual('blur'); + }); + + it('should set updateOn with nested groups', () => { + const g = new FormGroup( + { + group: new FormGroup({one: new FormControl(), two: new FormControl()}), + }, + {updateOn: 'blur'}); + + expect(g.get('group.one') !.updateOn).toEqual('blur'); + expect(g.get('group.two') !.updateOn).toEqual('blur'); + expect(g.get('group') !.updateOn).toEqual('blur'); + }); + + it('should set updateOn with nested arrays', () => { + const g = new FormGroup( + { + arr: new FormArray([new FormControl(), new FormControl()]), + }, + {updateOn: 'blur'}); + + expect(g.get(['arr', 0]) !.updateOn).toEqual('blur'); + expect(g.get(['arr', 1]) !.updateOn).toEqual('blur'); + expect(g.get('arr') !.updateOn).toEqual('blur'); + }); + + it('should allow control updateOn to override group updateOn', () => { + const g = new FormGroup( + {one: new FormControl('', {updateOn: 'change'}), two: new FormControl()}, + {updateOn: 'blur'}); + + expect(g.get('one') !.updateOn).toEqual('change'); + expect(g.get('two') !.updateOn).toEqual('blur'); + }); + + it('should set updateOn with complex setup', () => { + const g = new FormGroup({ + group: new FormGroup( + {one: new FormControl('', {updateOn: 'change'}), two: new FormControl()}, + {updateOn: 'blur'}), + groupTwo: new FormGroup({one: new FormControl()}, {updateOn: 'submit'}), + three: new FormControl() + }); + + expect(g.get('group.one') !.updateOn).toEqual('change'); + expect(g.get('group.two') !.updateOn).toEqual('blur'); + expect(g.get('groupTwo.one') !.updateOn).toEqual('submit'); + expect(g.get('three') !.updateOn).toEqual('change'); + }); + + }); }); diff --git a/packages/forms/test/reactive_integration_spec.ts b/packages/forms/test/reactive_integration_spec.ts index 4db317ca6e..a37ea8f458 100644 --- a/packages/forms/test/reactive_integration_spec.ts +++ b/packages/forms/test/reactive_integration_spec.ts @@ -931,7 +931,6 @@ export function main() { sub.unsubscribe(); }); - it('should mark as pristine properly if pending dirty', () => { const fixture = initTest(FormControlComp); const control = new FormControl('', {updateOn: 'blur'}); @@ -955,6 +954,94 @@ export function main() { expect(control.dirty).toBe(false, 'Expected pending dirty value to reset.'); }); + it('should update on blur with group updateOn', () => { + const fixture = initTest(FormGroupComp); + const control = new FormControl('', Validators.required); + const formGroup = new FormGroup({login: control}, {updateOn: 'blur'}); + fixture.componentInstance.form = formGroup; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + input.value = 'Nancy'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + expect(control.value).toEqual('', 'Expected value to remain unchanged until blur.'); + expect(control.valid).toBe(false, 'Expected no validation to occur until blur.'); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(control.value) + .toEqual('Nancy', 'Expected value to change once control is blurred.'); + expect(control.valid).toBe(true, 'Expected validation to run once control is blurred.'); + + }); + + it('should update on blur with array updateOn', () => { + const fixture = initTest(FormArrayComp); + const control = new FormControl('', Validators.required); + const cityArray = new FormArray([control], {updateOn: 'blur'}); + const formGroup = new FormGroup({cities: cityArray}); + fixture.componentInstance.form = formGroup; + fixture.componentInstance.cityArray = cityArray; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + input.value = 'Nancy'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + expect(control.value).toEqual('', 'Expected value to remain unchanged until blur.'); + expect(control.valid).toBe(false, 'Expected no validation to occur until blur.'); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(control.value) + .toEqual('Nancy', 'Expected value to change once control is blurred.'); + expect(control.valid).toBe(true, 'Expected validation to run once control is blurred.'); + + }); + + + it('should allow child control updateOn blur to override group updateOn', () => { + const fixture = initTest(NestedFormGroupComp); + const loginControl = + new FormControl('', {validators: Validators.required, updateOn: 'change'}); + const passwordControl = new FormControl('', Validators.required); + const formGroup = new FormGroup( + {signin: new FormGroup({login: loginControl, password: passwordControl})}, + {updateOn: 'blur'}); + fixture.componentInstance.form = formGroup; + fixture.detectChanges(); + + const [loginInput, passwordInput] = fixture.debugElement.queryAll(By.css('input')); + loginInput.nativeElement.value = 'Nancy'; + dispatchEvent(loginInput.nativeElement, 'input'); + fixture.detectChanges(); + + expect(loginControl.value).toEqual('Nancy', 'Expected value change on input.'); + expect(loginControl.valid).toBe(true, 'Expected validation to run on input.'); + + passwordInput.nativeElement.value = 'Carson'; + dispatchEvent(passwordInput.nativeElement, 'input'); + fixture.detectChanges(); + + expect(passwordControl.value) + .toEqual('', 'Expected value to remain unchanged until blur.'); + expect(passwordControl.valid).toBe(false, 'Expected no validation to occur until blur.'); + + dispatchEvent(passwordInput.nativeElement, 'blur'); + fixture.detectChanges(); + + expect(passwordControl.value) + .toEqual('Carson', 'Expected value to change once control is blurred.'); + expect(passwordControl.valid) + .toBe(true, 'Expected validation to run once control is blurred.'); + }); + + }); describe('on submit', () => { @@ -1193,7 +1280,6 @@ export function main() { }); - it('should mark as untouched properly if pending touched', () => { const fixture = initTest(FormGroupComp); const formGroup = new FormGroup({login: new FormControl('', {updateOn: 'submit'})}); @@ -1216,6 +1302,99 @@ export function main() { expect(formGroup.touched).toBe(false, 'Expected touched to stay false on submit.'); }); + it('should update on submit with group updateOn', () => { + const fixture = initTest(FormGroupComp); + const control = new FormControl('', Validators.required); + const formGroup = new FormGroup({login: control}, {updateOn: 'submit'}); + fixture.componentInstance.form = formGroup; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + input.value = 'Nancy'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + expect(control.value).toEqual('', 'Expected value to remain unchanged until submit.'); + expect(control.valid).toBe(false, 'Expected no validation to occur until submit.'); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(control.value).toEqual('', 'Expected value to remain unchanged until submit.'); + expect(control.valid).toBe(false, 'Expected no validation to occur until submit.'); + + const form = fixture.debugElement.query(By.css('form')).nativeElement; + dispatchEvent(form, 'submit'); + fixture.detectChanges(); + + expect(control.value).toEqual('Nancy', 'Expected value to change on submit.'); + expect(control.valid).toBe(true, 'Expected validation to run on submit.'); + + }); + + it('should update on submit with array updateOn', () => { + const fixture = initTest(FormArrayComp); + const control = new FormControl('', Validators.required); + const cityArray = new FormArray([control], {updateOn: 'submit'}); + const formGroup = new FormGroup({cities: cityArray}); + fixture.componentInstance.form = formGroup; + fixture.componentInstance.cityArray = cityArray; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')).nativeElement; + input.value = 'Nancy'; + dispatchEvent(input, 'input'); + fixture.detectChanges(); + + expect(control.value).toEqual('', 'Expected value to remain unchanged until submit.'); + expect(control.valid).toBe(false, 'Expected no validation to occur until submit.'); + + + const form = fixture.debugElement.query(By.css('form')).nativeElement; + dispatchEvent(form, 'submit'); + fixture.detectChanges(); + + expect(control.value).toEqual('Nancy', 'Expected value to change once control on submit'); + expect(control.valid).toBe(true, 'Expected validation to run on submit.'); + + }); + + it('should allow child control updateOn submit to override group updateOn', () => { + const fixture = initTest(NestedFormGroupComp); + const loginControl = + new FormControl('', {validators: Validators.required, updateOn: 'change'}); + const passwordControl = new FormControl('', Validators.required); + const formGroup = new FormGroup( + {signin: new FormGroup({login: loginControl, password: passwordControl})}, + {updateOn: 'submit'}); + fixture.componentInstance.form = formGroup; + fixture.detectChanges(); + + const [loginInput, passwordInput] = fixture.debugElement.queryAll(By.css('input')); + loginInput.nativeElement.value = 'Nancy'; + dispatchEvent(loginInput.nativeElement, 'input'); + fixture.detectChanges(); + + expect(loginControl.value).toEqual('Nancy', 'Expected value change on input.'); + expect(loginControl.valid).toBe(true, 'Expected validation to run on input.'); + + passwordInput.nativeElement.value = 'Carson'; + dispatchEvent(passwordInput.nativeElement, 'input'); + fixture.detectChanges(); + + expect(passwordControl.value) + .toEqual('', 'Expected value to remain unchanged until submit.'); + expect(passwordControl.valid) + .toBe(false, 'Expected no validation to occur until submit.'); + + const form = fixture.debugElement.query(By.css('form')).nativeElement; + dispatchEvent(form, 'submit'); + fixture.detectChanges(); + + expect(passwordControl.value).toEqual('Carson', 'Expected value to change on submit.'); + expect(passwordControl.valid).toBe(true, 'Expected validation to run on submit.'); + }); + }); }); @@ -2008,13 +2187,13 @@ class NestedFormGroupComp { @Component({ selector: 'form-array-comp', template: ` -
+
-
` + ` }) class FormArrayComp { form: FormGroup; diff --git a/tools/public_api_guard/forms/forms.d.ts b/tools/public_api_guard/forms/forms.d.ts index e82c9caf13..e5d27c631f 100644 --- a/tools/public_api_guard/forms/forms.d.ts +++ b/tools/public_api_guard/forms/forms.d.ts @@ -14,6 +14,7 @@ export declare abstract class AbstractControl { readonly statusChanges: Observable; readonly touched: boolean; readonly untouched: boolean; + readonly updateOn: FormHooks; readonly valid: boolean; validator: ValidatorFn | null; readonly value: any; From f0ec31e47f343283335046a8af1ef5f44abc34c0 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Wed, 9 Aug 2017 16:04:57 -0700 Subject: [PATCH 12/83] release: cut the 5.0.0-beta.3 release --- package.json | 2 +- packages/compiler-cli/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 41e326ee97..6801b85669 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-srcs", - "version": "5.0.0-beta.2", + "version": "5.0.0-beta.3", "private": true, "branchPattern": "2.0.*", "description": "Angular - a web framework for modern web apps", diff --git a/packages/compiler-cli/package.json b/packages/compiler-cli/package.json index ef1ce4b573..67d7093596 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.2", + "@angular/tsc-wrapped": "5.0.0-beta.3", "reflect-metadata": "^0.1.2", "minimist": "^1.2.0" }, From cea02414b0302b9e3f5f43f2b3f05c03f5206238 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Wed, 9 Aug 2017 16:07:37 -0700 Subject: [PATCH 13/83] docs: add changelog for 5.0.0-beta.3 --- CHANGELOG.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b46f8db760..c633364927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,70 @@ + +# [5.0.0-beta.3](https://github.com/angular/angular/compare/5.0.0-beta.2...5.0.0-beta.3) (2017-08-09) + + +### Bug Fixes + +* **animations:** revert container/queried animations accordingly during cancel ([#18516](https://github.com/angular/angular/issues/18516)) ([c0c03dc](https://github.com/angular/angular/commit/c0c03dc)) +* **animations:** support persisting dynamic styles within animation states ([#18468](https://github.com/angular/angular/issues/18468)) ([05472cb](https://github.com/angular/angular/commit/05472cb)), closes [#18423](https://github.com/angular/angular/issues/18423) [#17505](https://github.com/angular/angular/issues/17505) +* **benchpress:** compile cleanly with TS 2.4 ([#18455](https://github.com/angular/angular/issues/18455)) ([e25b3dd](https://github.com/angular/angular/commit/e25b3dd)) +* **common:** don't recreate view when context shape doesn't change ([#18277](https://github.com/angular/angular/issues/18277)) ([685cc26](https://github.com/angular/angular/commit/685cc26)), closes [#13407](https://github.com/angular/angular/issues/13407) +* **compiler:** cleanly compile with TypeScript 2.4 ([#18456](https://github.com/angular/angular/issues/18456)) ([7c47b62](https://github.com/angular/angular/commit/7c47b62)) +* **compiler:** ignore [@import](https://github.com/import) in multi-line css ([#18452](https://github.com/angular/angular/issues/18452)) ([1dca575](https://github.com/angular/angular/commit/1dca575)), closes [#18038](https://github.com/angular/angular/issues/18038) +* **compiler-cli:** disable buggy expression lowering ([#18513](https://github.com/angular/angular/issues/18513)) ([ca695e0](https://github.com/angular/angular/commit/ca695e0)) +* **compiler-cli:** fix and re-enable expression lowering ([#18570](https://github.com/angular/angular/issues/18570)) ([6f2038c](https://github.com/angular/angular/commit/6f2038c)), closes [#18388](https://github.com/angular/angular/issues/18388) +* **compiler-cli:** modified ngc to throw all errors, not just syntax ([#18388](https://github.com/angular/angular/issues/18388)) ([5651e4a](https://github.com/angular/angular/commit/5651e4a)) +* **compiler-cli:** remove minimist dependency of compiler-cli/index ([#18532](https://github.com/angular/angular/issues/18532)) ([5b7432b](https://github.com/angular/angular/commit/5b7432b)) +* **core:** fix platform-browser-dynamic ([#18576](https://github.com/angular/angular/issues/18576)) ([f0a5501](https://github.com/angular/angular/commit/f0a5501)) +* **core:** forbid destroyed views to be inserted or moved in VC ([#18568](https://github.com/angular/angular/issues/18568)) ([e54bd59](https://github.com/angular/angular/commit/e54bd59)), closes [#17780](https://github.com/angular/angular/issues/17780) + +### Features + +* **core:** Create StaticInjector which does not depend on Reflect polyfill. ([d9d00bd](https://github.com/angular/angular/commit/d9d00bd)) +* **forms:** add default updateOn values for groups and arrays ([#18536](https://github.com/angular/angular/issues/18536)) ([ff5c58b](https://github.com/angular/angular/commit/ff5c58b)) +* **forms:** add updateOn blur option to FormControls ([#18408](https://github.com/angular/angular/issues/18408)) ([333a708](https://github.com/angular/angular/commit/333a708)), closes [#7113](https://github.com/angular/angular/issues/7113) +* **forms:** add updateOn submit option to FormControls ([#18514](https://github.com/angular/angular/issues/18514)) ([f69561b](https://github.com/angular/angular/commit/f69561b)) + +### Performance Improvements + +* switch angular to use StaticInjector instead of ReflectiveInjector ([fcadbf4](https://github.com/angular/angular/commit/fcadbf4)), closes [#18496](https://github.com/angular/angular/issues/18496) + + +### BREAKING CHANGES + +* `platformXXXX()` no longer accepts providers which depend on reflection. +Specifically the method signature when from `Provider[]` to +`StaticProvider[]`. + +Example: +Before: +``` +[ + MyClass, + {provide: ClassA, useClass: SubClassA} +] + +``` + +After: +``` +[ + {provide: MyClass, deps: [Dep1,...]}, + {provide: ClassA, useClass: SubClassA, deps: [Dep1,...]} +] +``` + +NOTE: This only applies to platform creation and providers for the JIT +compiler. It does not apply to `@Component` or `@NgModule` provides +declarations. + +Benchpress note: Previously Benchpress also supported reflective +provides, which now require static providers. + +DEPRECATION: + +- `ReflectiveInjector` is now deprecated as it will be remove. Use + `Injector.create` as a replacement. + # [5.0.0-beta.2](https://github.com/angular/angular/compare/5.0.0-beta.1...5.0.0-beta.2) (2017-08-02) From 4d523fda98ff007a28ae110475dfc6aa017c6b72 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Thu, 10 Aug 2017 03:54:10 +0300 Subject: [PATCH 14/83] fix(aio): fix compilation error by using the correct type for `providers` --- aio/content/examples/i18n/src/app/i18n-providers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aio/content/examples/i18n/src/app/i18n-providers.ts b/aio/content/examples/i18n/src/app/i18n-providers.ts index 0e820bbcfd..3c74d0e8e0 100644 --- a/aio/content/examples/i18n/src/app/i18n-providers.ts +++ b/aio/content/examples/i18n/src/app/i18n-providers.ts @@ -1,15 +1,15 @@ // #docplaster // #docregion without-missing-translation -import { TRANSLATIONS, TRANSLATIONS_FORMAT, LOCALE_ID, MissingTranslationStrategy } from '@angular/core'; +import { TRANSLATIONS, TRANSLATIONS_FORMAT, LOCALE_ID, MissingTranslationStrategy, StaticProvider } from '@angular/core'; import { CompilerConfig } from '@angular/compiler'; -export function getTranslationProviders(): Promise { +export function getTranslationProviders(): Promise { // Get the locale id from the global const locale = document['locale'] as string; // return no providers if fail to get translation file for locale - const noProviders: Object[] = []; + const noProviders: StaticProvider[] = []; // No locale or U.S. English: no translation providers if (!locale || locale === 'en-US') { From 06faac8b5cb08cf838a39f2666ebf473f0f063ec Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Thu, 3 Aug 2017 11:40:39 +0300 Subject: [PATCH 15/83] fix(aio): skip PWA test when redeploying non-public commit --- .../scripts-js/lib/upload-server/build-creator.ts | 3 ++- .../lib/verify-setup/upload-server.e2e.ts | 7 ++++--- .../test/upload-server/build-creator.spec.ts | 6 ++++-- .../dockerbuild/scripts-js/tslint.json | 2 +- .../docs/overview--http-status-codes.md | 7 ++++--- aio/scripts/deploy-preview.sh | 14 +++++++------- 6 files changed, 22 insertions(+), 17 deletions(-) diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-creator.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-creator.ts index 183bf9a196..cfb3ba616f 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-creator.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-creator.ts @@ -32,7 +32,8 @@ export class BuildCreator extends EventEmitter { then(() => Promise.all([this.exists(prDir), this.exists(shaDir)])). then(([prDirExisted, shaDirExisted]) => { if (shaDirExisted) { - throw new UploadError(409, `Request to overwrite existing directory: ${shaDir}`); + const publicOrNot = isPublic ? 'public' : 'non-public'; + throw new UploadError(409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`); } dirToRemoveOnError = prDirExisted ? shaDir : prDir; diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/upload-server.e2e.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/upload-server.e2e.ts index 6191a495f4..002d93de3e 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/upload-server.e2e.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/upload-server.e2e.ts @@ -110,6 +110,7 @@ describe('upload-server (on HTTP)', () => { const authorizationHeader2 = isPublic ? authorizationHeader : `--header "Authorization: ${c.BV_verify_verifiedNotTrusted}"`; const cmdPrefix = curl('', `${authorizationHeader2} ${xFileHeader}`); + const overwriteRe = RegExp(`^Request to overwrite existing ${isPublic ? 'public' : 'non-public'} directory`); it('should not overwrite existing builds', done => { @@ -120,7 +121,7 @@ describe('upload-server (on HTTP)', () => { expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content'); h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`). - then(h.verifyResponse(409, /^Request to overwrite existing directory/)). + then(h.verifyResponse(409, overwriteRe)). then(() => expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content')). then(done); }); @@ -141,7 +142,7 @@ describe('upload-server (on HTTP)', () => { expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content'); h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9Almost}`). - then(h.verifyResponse(409, /^Request to overwrite existing directory/)). + then(h.verifyResponse(409, overwriteRe)). then(() => expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content')). then(done); }); @@ -310,7 +311,7 @@ describe('upload-server (on HTTP)', () => { expect(h.buildExists(pr, sha0, isPublic)).toBe(false); uploadBuild(sha0). - then(h.verifyResponse(409, /^Request to overwrite existing directory/)). + then(h.verifyResponse(409, overwriteRe)). then(() => { checkPrVisibility(isPublic); expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).toContain(pr); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-creator.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-creator.spec.ts index 6125205ba3..d5b1f91576 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-creator.spec.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-creator.spec.ts @@ -153,7 +153,8 @@ describe('BuildCreator', () => { it('should abort and skip further operations if the build does already exist', done => { existsValues[shaDir] = true; bc.create(pr, sha, archive, isPublic).catch(err => { - expectToBeUploadError(err, 409, `Request to overwrite existing directory: ${shaDir}`); + const publicOrNot = isPublic ? 'public' : 'non-public'; + expectToBeUploadError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`); expect(shellMkdirSpy).not.toHaveBeenCalled(); expect(bcExtractArchiveSpy).not.toHaveBeenCalled(); expect(bcEmitSpy).not.toHaveBeenCalled(); @@ -169,7 +170,8 @@ describe('BuildCreator', () => { expect(bcExistsSpy(shaDir)).toBe(false); bc.create(pr, sha, archive, isPublic).catch(err => { - expectToBeUploadError(err, 409, `Request to overwrite existing directory: ${shaDir}`); + const publicOrNot = isPublic ? 'public' : 'non-public'; + expectToBeUploadError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`); expect(shellMkdirSpy).not.toHaveBeenCalled(); expect(bcExtractArchiveSpy).not.toHaveBeenCalled(); expect(bcEmitSpy).not.toHaveBeenCalled(); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/tslint.json b/aio/aio-builds-setup/dockerbuild/scripts-js/tslint.json index d773f53668..59d3ff1d9f 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/tslint.json +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/tslint.json @@ -6,7 +6,7 @@ "interface-name": [true, "never-prefix"], "max-classes-per-file": [true, 4], "no-consecutive-blank-lines": [true, 2], - "no-console": false, + "no-console": [false], "no-namespace": [true, "allow-declarations"], "no-string-literal": false, "quotemark": [true, "single"], diff --git a/aio/aio-builds-setup/docs/overview--http-status-codes.md b/aio/aio-builds-setup/docs/overview--http-status-codes.md index 54b06f66e8..1a041ffbcf 100644 --- a/aio/aio-builds-setup/docs/overview--http-status-codes.md +++ b/aio/aio-builds-setup/docs/overview--http-status-codes.md @@ -46,8 +46,8 @@ with a bried explanation of what they mean: Request method other than POST. - **409 (Conflict)**: - Request to overwrite existing directory (e.g. deploy existing build or change PR visibility when - the destination directory does already exist). + Request to overwrite existing (public or non-public) directory (e.g. deploy existing build or + change PR visibility when the destination directory does already exist). - **413 (Payload Too Large)**: Payload larger than size specified in `AIO_UPLOAD_MAX_SIZE`. @@ -71,7 +71,8 @@ with a bried explanation of what they mean: Request method other than POST. - **409 (Conflict)**: - Request to overwrite existing directory (i.e. directories for both visibilities exist). + Request to overwrite existing (public or non-public) directory (i.e. directories for both + visibilities exist). (Normally, this should not happen.) diff --git a/aio/scripts/deploy-preview.sh b/aio/scripts/deploy-preview.sh index 4afd49a5cf..e49bd99482 100755 --- a/aio/scripts/deploy-preview.sh +++ b/aio/scripts/deploy-preview.sh @@ -26,31 +26,31 @@ readonly relevantChangedFilesCount=$(git diff --name-only $TRAVIS_COMMIT_RANGE | fi # Build the app - if [ "$skipBuild" != "true" ]; then + if [[ "$skipBuild" != "true" ]]; then yarn build fi tar --create --gzip --directory "$INPUT_DIR" --file "$OUTPUT_FILE" . yarn payload-size # Deploy to staging - readonly httpCode=$( + readonly output=$( curl --include --location --request POST --silent --write-out "\nHTTP_CODE: %{http_code}\n" \ --header "Authorization: Token $NGBUILDS_IO_KEY" --data-binary "@$OUTPUT_FILE" "$UPLOAD_URL" \ | sed 's/\r\n/\n/' \ - | tee /dev/fd/3 \ - | tail -1 \ - | sed 's/HTTP_CODE: //' + | tee /dev/fd/3 ) + readonly isHidden=$([[ `echo $output | grep 'non-public'` ]] && echo "true" || echo "") + readonly httpCode=$(echo "$output" | tail -1 | sed 's/HTTP_CODE: //') # Exit with an error if the request failed. # (Ignore 409 failures, which mean trying to re-deploy for the same PR/SHA.) - if [ $httpCode -lt 200 ] || ([ $httpCode -ge 400 ] && [ $httpCode -ne 409 ]); then + if [[ $httpCode -lt 200 ]] || ([[ $httpCode -ge 400 ]] && [[ $httpCode -ne 409 ]]); then exit 1 fi # Run PWA-score tests (unless the deployment is not public yet; # i.e. it could not be automatically verified). - if [ $httpCode -ne 202 ]; then + if [[ $httpCode -ne 202 ]] && [[ "$isHidden" != "true" ]]; then yarn test-pwa-score -- "$DEPLOYED_URL" "$MIN_PWA_SCORE" fi ) From 679608db6543c2f9633e33841a6074548006fbe2 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Wed, 2 Aug 2017 11:20:07 -0700 Subject: [PATCH 16/83] refactor(compiler-cli): use the transformer based compiler by default The source map does not currently work with the transformer pipeline. It will be re-enabled after TypeScript 2.4 is made the min version. To revert to the former compiler, use the `disableTransformerPipeline` in tsconfig.json: ``` { "angularCompilerOptions": { "disableTransformerPipeline": true } } ``` --- integration/hello_world__closure/package.json | 2 +- integration/i18n/closure.conf | 30 ++++ integration/i18n/e2e/app.e2e-spec.ts | 10 ++ integration/i18n/e2e/browser.config.json | 15 ++ integration/i18n/e2e/protractor.config.js | 16 ++ integration/i18n/e2e/tsconfig.json | 8 + integration/i18n/package.json | 32 ++++ integration/i18n/src/app.ts | 11 ++ integration/i18n/src/hello-world.component.ts | 9 + integration/i18n/src/index.html | 18 ++ integration/i18n/src/main.ts | 4 + integration/i18n/tsconfig.json | 30 ++++ .../integrationtest/alt/src/bootstrap.ts | 12 -- .../integrationtest/test/basic_spec.ts | 11 +- .../integrationtest/test/source_map_spec.ts | 3 +- .../integrationtest/test/util_alt.ts | 33 ---- .../integrationtest/tsconfig-build-alt.json | 32 ---- .../integrationtest/tsconfig-build.json | 4 +- packages/compiler-cli/package.json | 3 +- .../src/diagnostics/expression_diagnostics.ts | 6 +- packages/compiler-cli/src/main.ts | 24 ++- packages/compiler-cli/src/ngc.ts | 34 +++- packages/compiler-cli/src/perform-compile.ts | 60 +++---- packages/compiler-cli/src/transformers/api.ts | 35 +++- .../src/transformers/lower_expressions.ts | 22 ++- .../src/transformers/node_emitter.ts | 32 ++-- .../compiler-cli/src/transformers/program.ts | 170 ++++++++++++------ packages/compiler-cli/test/ngc_spec.ts | 124 +++++++++++-- .../test/transformers/node_emitter_spec.ts | 6 +- .../integrationtest/package.json | 2 +- scripts/ci/offline_compiler_test.sh | 5 +- tools/ngc-wrapped/index.ts | 13 +- 32 files changed, 569 insertions(+), 247 deletions(-) create mode 100644 integration/i18n/closure.conf create mode 100644 integration/i18n/e2e/app.e2e-spec.ts create mode 100644 integration/i18n/e2e/browser.config.json create mode 100644 integration/i18n/e2e/protractor.config.js create mode 100644 integration/i18n/e2e/tsconfig.json create mode 100644 integration/i18n/package.json create mode 100644 integration/i18n/src/app.ts create mode 100644 integration/i18n/src/hello-world.component.ts create mode 100644 integration/i18n/src/index.html create mode 100644 integration/i18n/src/main.ts create mode 100644 integration/i18n/tsconfig.json delete mode 100644 packages/compiler-cli/integrationtest/alt/src/bootstrap.ts delete mode 100644 packages/compiler-cli/integrationtest/test/util_alt.ts delete mode 100644 packages/compiler-cli/integrationtest/tsconfig-build-alt.json diff --git a/integration/hello_world__closure/package.json b/integration/hello_world__closure/package.json index b5cecce1de..14d555606d 100644 --- a/integration/hello_world__closure/package.json +++ b/integration/hello_world__closure/package.json @@ -13,7 +13,7 @@ "@angular/tsc-wrapped": "file:../../dist/packages-dist/tsc-wrapped", "google-closure-compiler": "20170409.0.0", "rxjs": "5.3.1", - "typescript": "2.1.6", + "typescript": "~2.3.1", "zone.js": "0.8.6" }, "devDependencies": { diff --git a/integration/i18n/closure.conf b/integration/i18n/closure.conf new file mode 100644 index 0000000000..8393a38b29 --- /dev/null +++ b/integration/i18n/closure.conf @@ -0,0 +1,30 @@ +--compilation_level=ADVANCED_OPTIMIZATIONS +--language_out=ES5 +--js_output_file=dist/bundle.js +--output_manifest=dist/manifest.MF +--variable_renaming_report=dist/variable_renaming_report +--property_renaming_report=dist/property_renaming_report +--create_source_map=%outname%.map + +--warning_level=QUIET +--dependency_mode=STRICT +--rewrite_polyfills=false + +node_modules/zone.js/dist/zone_externs.js + +--js node_modules/rxjs/**.js +--process_common_js_modules +--module_resolution=node + +node_modules/@angular/core/@angular/core.js +--js_module_root=node_modules/@angular/core +node_modules/@angular/core/src/testability/testability.externs.js + +node_modules/@angular/common/@angular/common.js +--js_module_root=node_modules/@angular/common + +node_modules/@angular/platform-browser/@angular/platform-browser.js +--js_module_root=node_modules/@angular/platform-browser + +--js built/**.js +--entry_point=built/src/main diff --git a/integration/i18n/e2e/app.e2e-spec.ts b/integration/i18n/e2e/app.e2e-spec.ts new file mode 100644 index 0000000000..afcc36025a --- /dev/null +++ b/integration/i18n/e2e/app.e2e-spec.ts @@ -0,0 +1,10 @@ +import { browser, element, by } from 'protractor'; + +describe('i18n E2E Tests', function () { + it('remove i18n attributes', function () { + browser.get(''); + const div = element(by.css('div')); + expect(div.getAttribute('title')).not.toBe(null); + expect(div.getAttribute('i18n')).toBe(null); + }); +}); diff --git a/integration/i18n/e2e/browser.config.json b/integration/i18n/e2e/browser.config.json new file mode 100644 index 0000000000..dcdae63008 --- /dev/null +++ b/integration/i18n/e2e/browser.config.json @@ -0,0 +1,15 @@ +{ + "open": false, + "logLevel": "silent", + "port": 8080, + "server": { + "baseDir": "src", + "routes": { + "/dist": "dist", + "/node_modules": "node_modules" + }, + "middleware": { + "0": null + } + } +} \ No newline at end of file diff --git a/integration/i18n/e2e/protractor.config.js b/integration/i18n/e2e/protractor.config.js new file mode 100644 index 0000000000..5bc4f6e640 --- /dev/null +++ b/integration/i18n/e2e/protractor.config.js @@ -0,0 +1,16 @@ +exports.config = { + specs: [ + '../built/e2e/*.e2e-spec.js' + ], + capabilities: { + browserName: 'chrome', + chromeOptions: { + args: ['--no-sandbox'], + binary: process.env.CHROME_BIN, + } + }, + directConnect: true, + baseUrl: 'http://localhost:8080/', + framework: 'jasmine', + useAllAngular2AppRoots: true +}; diff --git a/integration/i18n/e2e/tsconfig.json b/integration/i18n/e2e/tsconfig.json new file mode 100644 index 0000000000..e112859422 --- /dev/null +++ b/integration/i18n/e2e/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "outDir": "../built/e2e", + "types": ["jasmine"], + // TODO(alexeagle): was required for Protractor 4.0.11 + "skipLibCheck": true + } +} \ No newline at end of file diff --git a/integration/i18n/package.json b/integration/i18n/package.json new file mode 100644 index 0000000000..14d555606d --- /dev/null +++ b/integration/i18n/package.json @@ -0,0 +1,32 @@ +{ + "name": "angular-integration", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@angular/animations": "file:../../dist/packages-dist/animations", + "@angular/common": "file:../../dist/packages-dist/common", + "@angular/compiler": "file:../../dist/packages-dist/compiler", + "@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli", + "@angular/core": "file:../../dist/packages-dist/core", + "@angular/platform-browser": "file:../../dist/packages-dist/platform-browser", + "@angular/platform-server": "file:../../dist/packages-dist/platform-server", + "@angular/tsc-wrapped": "file:../../dist/packages-dist/tsc-wrapped", + "google-closure-compiler": "20170409.0.0", + "rxjs": "5.3.1", + "typescript": "~2.3.1", + "zone.js": "0.8.6" + }, + "devDependencies": { + "@types/jasmine": "2.5.41", + "concurrently": "3.4.0", + "lite-server": "2.2.2", + "protractor": "file:../../node_modules/protractor" + }, + "scripts": { + "closure": "java -jar node_modules/google-closure-compiler/compiler.jar --flagfile closure.conf", + "test": "ngc && yarn run closure && concurrently \"yarn run serve\" \"yarn run protractor\" --kill-others --success first", + "serve": "lite-server -c e2e/browser.config.json", + "preprotractor": "tsc -p e2e", + "protractor": "protractor e2e/protractor.config.js" + } +} \ No newline at end of file diff --git a/integration/i18n/src/app.ts b/integration/i18n/src/app.ts new file mode 100644 index 0000000000..31ecac712b --- /dev/null +++ b/integration/i18n/src/app.ts @@ -0,0 +1,11 @@ +import {HelloWorldComponent} from './hello-world.component'; + +import {NgModule} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; + +@NgModule({ + declarations: [HelloWorldComponent], + bootstrap: [HelloWorldComponent], + imports: [BrowserModule], +}) +export class AppModule {} diff --git a/integration/i18n/src/hello-world.component.ts b/integration/i18n/src/hello-world.component.ts new file mode 100644 index 0000000000..5aabbddd93 --- /dev/null +++ b/integration/i18n/src/hello-world.component.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'hello-world-app', + template: `
Hello {{ name }}!
`, +}) +export class HelloWorldComponent { + name: string = 'world'; +} diff --git a/integration/i18n/src/index.html b/integration/i18n/src/index.html new file mode 100644 index 0000000000..ffbd2616e9 --- /dev/null +++ b/integration/i18n/src/index.html @@ -0,0 +1,18 @@ + + + + + + Hello World + + + + + Loading... + + + + + + + \ No newline at end of file diff --git a/integration/i18n/src/main.ts b/integration/i18n/src/main.ts new file mode 100644 index 0000000000..81d2aa7b15 --- /dev/null +++ b/integration/i18n/src/main.ts @@ -0,0 +1,4 @@ +import {platformBrowser} from '@angular/platform-browser'; +import {AppModuleNgFactory} from './app.ngfactory'; + +platformBrowser().bootstrapModuleFactory(AppModuleNgFactory); diff --git a/integration/i18n/tsconfig.json b/integration/i18n/tsconfig.json new file mode 100644 index 0000000000..34cc0b9fb2 --- /dev/null +++ b/integration/i18n/tsconfig.json @@ -0,0 +1,30 @@ +{ + "angularCompilerOptions": { + "annotationsAs": "static fields", + "annotateForClosureCompiler": true, + "alwaysCompileGeneratedCode": true + }, + + "compilerOptions": { + "module": "es2015", + "moduleResolution": "node", + // TODO(i): strictNullChecks should turned on but are temporarily disabled due to #15432 + "strictNullChecks": false, + "target": "es6", + "noImplicitAny": false, + "sourceMap": false, + "experimentalDecorators": true, + "outDir": "built", + "rootDir": ".", + "declaration": true, + "types": [] + }, + + "exclude": [ + "vendor", + "node_modules", + "built", + "dist", + "e2e" + ] +} \ No newline at end of file diff --git a/packages/compiler-cli/integrationtest/alt/src/bootstrap.ts b/packages/compiler-cli/integrationtest/alt/src/bootstrap.ts deleted file mode 100644 index d6d7aedc7f..0000000000 --- a/packages/compiler-cli/integrationtest/alt/src/bootstrap.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @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 {BasicComp} from '../../src/basic'; -import {MainModuleNgFactory} from './module.ngfactory'; - -MainModuleNgFactory.create(null).instance.appRef.bootstrap(BasicComp); diff --git a/packages/compiler-cli/integrationtest/test/basic_spec.ts b/packages/compiler-cli/integrationtest/test/basic_spec.ts index e48e60074d..e9e7f0ac46 100644 --- a/packages/compiler-cli/integrationtest/test/basic_spec.ts +++ b/packages/compiler-cli/integrationtest/test/basic_spec.ts @@ -12,7 +12,6 @@ import * as path from 'path'; import {MultipleComponentsMyComp} from '../src/a/multiple_components'; import {BasicComp} from '../src/basic'; import {createComponent} from './util'; -import {createComponentAlt} from './util_alt'; describe('template codegen output', () => { const outDir = 'src'; @@ -37,9 +36,9 @@ describe('template codegen output', () => { expect(fs.readFileSync(dtsOutput, {encoding: 'utf-8'})).toContain('Basic'); }); - it('should write .ngfactory.ts for .d.ts inputs', () => { + it('should write .ngfactory.js for .d.ts inputs', () => { const factoryOutput = - path.join('node_modules', '@angular2-material', 'button', 'button.ngfactory.ts'); + path.join('node_modules', '@angular2-material', 'button', 'button.ngfactory.js'); expect(fs.existsSync(factoryOutput)).toBeTruthy(); }); @@ -95,11 +94,5 @@ describe('template codegen output', () => { expect(containerElement.attributes['title']).toBe('käännä teksti'); expect(containerElement.attributes['i18n-title']).toBeUndefined(); }); - - it('should have removed i18n markup event without translations', () => { - const containerElement = createComponentAlt(BasicComp).debugElement.children[0]; - expect(containerElement.attributes['title']).toBe('translate me'); - expect(containerElement.attributes['i18n-title']).toBeUndefined(); - }); }); }); diff --git a/packages/compiler-cli/integrationtest/test/source_map_spec.ts b/packages/compiler-cli/integrationtest/test/source_map_spec.ts index 431cccc4dd..25b4dd7b1b 100644 --- a/packages/compiler-cli/integrationtest/test/source_map_spec.ts +++ b/packages/compiler-cli/integrationtest/test/source_map_spec.ts @@ -10,7 +10,8 @@ import './init'; import {BindingErrorComp} from '../src/errors'; import {createComponent} from './util'; -describe('source maps', () => { +// TODO(tbosch): source maps does not currently work with the transformer pipeline +xdescribe('source maps', () => { it('should report source location for binding errors', () => { const comp = createComponent(BindingErrorComp); let error: any; diff --git a/packages/compiler-cli/integrationtest/test/util_alt.ts b/packages/compiler-cli/integrationtest/test/util_alt.ts deleted file mode 100644 index 972dc03ac9..0000000000 --- a/packages/compiler-cli/integrationtest/test/util_alt.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @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 {NgModuleRef} from '@angular/core'; -import {ComponentFixture} from '@angular/core/testing'; -import {platformServerTesting} from '@angular/platform-server/testing'; - -import {MainModuleNgFactory} from '../alt/src/module.ngfactory'; -import {MainModule} from '../src/module'; - -let mainModuleRef: NgModuleRef = null !; -beforeEach((done) => { - platformServerTesting().bootstrapModuleFactory(MainModuleNgFactory).then((moduleRef: any) => { - mainModuleRef = moduleRef; - done(); - }); -}); - -export function createModule(): NgModuleRef { - return mainModuleRef; -} - -export function createComponentAlt(comp: {new (...args: any[]): C}): ComponentFixture { - const moduleRef = createModule(); - const compRef = - moduleRef.componentFactoryResolver.resolveComponentFactory(comp).create(moduleRef.injector); - return new ComponentFixture(compRef, null, false); -} diff --git a/packages/compiler-cli/integrationtest/tsconfig-build-alt.json b/packages/compiler-cli/integrationtest/tsconfig-build-alt.json deleted file mode 100644 index 2a1553a619..0000000000 --- a/packages/compiler-cli/integrationtest/tsconfig-build-alt.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "angularCompilerOptions": { - // For TypeScript 1.8, we have to lay out generated files - // in the same source directory with your code. - "genDir": "./alt", - "debug": true, - "enableSummariesForJit": true, - "alwaysCompileGeneratedCode": true - }, - - "compilerOptions": { - "target": "es5", - "experimentalDecorators": true, - "noImplicitAny": true, - "strictNullChecks": true, - "skipLibCheck": true, - "moduleResolution": "node", - "rootDir": "", - "declaration": true, - "lib": ["es6", "dom"], - "baseUrl": ".", - // Prevent scanning up the directory tree for types - "typeRoots": ["node_modules/@types"], - "noUnusedLocals": true, - "sourceMap": true - }, - - "files": [ - "src/module", - "alt/src/bootstrap" - ] -} diff --git a/packages/compiler-cli/integrationtest/tsconfig-build.json b/packages/compiler-cli/integrationtest/tsconfig-build.json index 2928fa0a65..5c753f89ba 100644 --- a/packages/compiler-cli/integrationtest/tsconfig-build.json +++ b/packages/compiler-cli/integrationtest/tsconfig-build.json @@ -5,7 +5,9 @@ "genDir": ".", "debug": true, "enableSummariesForJit": true, - "alwaysCompileGeneratedCode": true + "alwaysCompileGeneratedCode": true, + "locale": "fi", + "i18nFormat": "xlf" }, "compilerOptions": { diff --git a/packages/compiler-cli/package.json b/packages/compiler-cli/package.json index 67d7093596..c11e9febf0 100644 --- a/packages/compiler-cli/package.json +++ b/packages/compiler-cli/package.json @@ -11,7 +11,8 @@ "dependencies": { "@angular/tsc-wrapped": "5.0.0-beta.3", "reflect-metadata": "^0.1.2", - "minimist": "^1.2.0" + "minimist": "^1.2.0", + "tsickle": "^0.23.4" }, "peerDependencies": { "typescript": "^2.0.2", diff --git a/packages/compiler-cli/src/diagnostics/expression_diagnostics.ts b/packages/compiler-cli/src/diagnostics/expression_diagnostics.ts index 3a60d67709..4bc0721c60 100644 --- a/packages/compiler-cli/src/diagnostics/expression_diagnostics.ts +++ b/packages/compiler-cli/src/diagnostics/expression_diagnostics.ts @@ -56,7 +56,7 @@ function getReferences(info: DiagnosticTemplateInfo): SymbolDeclaration[] { name: reference.name, kind: 'reference', type: type || info.query.getBuiltinType(BuiltinType.Any), - get definition() { return getDefintionOf(info, reference); } + get definition() { return getDefinitionOf(info, reference); } }); } } @@ -77,7 +77,7 @@ function getReferences(info: DiagnosticTemplateInfo): SymbolDeclaration[] { return result; } -function getDefintionOf(info: DiagnosticTemplateInfo, ast: TemplateAst): Definition|undefined { +function getDefinitionOf(info: DiagnosticTemplateInfo, ast: TemplateAst): Definition|undefined { if (info.fileName) { const templateOffset = info.offset; return [{ @@ -124,7 +124,7 @@ function getVarDeclarations( } result.push({ name, - kind: 'variable', type, get definition() { return getDefintionOf(info, variable); } + kind: 'variable', type, get definition() { return getDefinitionOf(info, variable); } }); } } diff --git a/packages/compiler-cli/src/main.ts b/packages/compiler-cli/src/main.ts index 3dc68977d9..9e4f171760 100644 --- a/packages/compiler-cli/src/main.ts +++ b/packages/compiler-cli/src/main.ts @@ -7,14 +7,19 @@ * found in the LICENSE file at https://angular.io/license */ - // Must be imported first, because Angular decorators throw on load. import 'reflect-metadata'; import * as ts from 'typescript'; import * as tsc from '@angular/tsc-wrapped'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as ngc from './ngc'; + import {isSyntaxError} from '@angular/compiler'; +import {readConfiguration} from './perform-compile'; + import {CodeGenerator} from './codegen'; function codegen( @@ -46,6 +51,19 @@ export function main( // CLI entry point if (require.main === module) { - const args = require('minimist')(process.argv.slice(2)); - main(args).then((exitCode: number) => process.exit(exitCode)); + const args = process.argv.slice(2); + const parsedArgs = require('minimist')(args); + const project = parsedArgs.p || parsedArgs.project || '.'; + + const projectDir = fs.lstatSync(project).isFile() ? path.dirname(project) : project; + + // file names in tsconfig are resolved relative to this absolute path + const basePath = path.resolve(process.cwd(), projectDir); + const {ngOptions} = readConfiguration(project, basePath); + + if (ngOptions.disableTransformerPipeline) { + main(parsedArgs).then((exitCode: number) => process.exit(exitCode)); + } else { + process.exit(ngc.main(args, s => console.error(s))); + } } diff --git a/packages/compiler-cli/src/ngc.ts b/packages/compiler-cli/src/ngc.ts index 658ed31d6e..788b1f4cb1 100644 --- a/packages/compiler-cli/src/ngc.ts +++ b/packages/compiler-cli/src/ngc.ts @@ -9,11 +9,12 @@ // Must be imported first, because Angular decorators throw on load. import 'reflect-metadata'; -import {isSyntaxError, syntaxError} from '@angular/compiler'; +import {isSyntaxError} from '@angular/compiler'; import * as fs from 'fs'; import * as path from 'path'; import {performCompilation, readConfiguration, throwOnDiagnostics} from './perform-compile'; +import {CompilerOptions} from './transformers/api'; export function main( args: string[], consoleError: (s: string) => void = console.error, @@ -27,16 +28,41 @@ export function main( // file names in tsconfig are resolved relative to this absolute path const basePath = path.resolve(process.cwd(), projectDir); const {parsed, ngOptions} = readConfiguration(project, basePath, checkFunc); - return performCompilation( - basePath, parsed.fileNames, parsed.options, ngOptions, consoleError, checkFunc); + + // CLI arguments can override the i18n options + const ngcOptions = mergeCommandLine(parsedArgs, ngOptions); + + const res = performCompilation( + basePath, parsed.fileNames, parsed.options, ngcOptions, consoleError, checkFunc); + + return res.errorCode; } catch (e) { + if (isSyntaxError(e)) { + consoleError(e.message); + return 1; + } + consoleError(e.stack); consoleError('Compilation failed'); return 2; } } +// Merge command line parameters +function mergeCommandLine( + parsedArgs: {[k: string]: string}, options: CompilerOptions): CompilerOptions { + if (parsedArgs.i18nFile) options.i18nInFile = parsedArgs.i18nFile; + if (parsedArgs.i18nFormat) options.i18nInFormat = parsedArgs.i18nFormat; + if (parsedArgs.locale) options.i18nInLocale = parsedArgs.locale; + const mt = parsedArgs.missingTranslation; + if (mt === 'error' || mt === 'warning' || mt === 'ignore') { + options.i18nInMissingTranslations = mt; + } + + return options; +} + // CLI entry point if (require.main === module) { process.exit(main(process.argv.slice(2), s => console.error(s))); -} +} \ No newline at end of file diff --git a/packages/compiler-cli/src/perform-compile.ts b/packages/compiler-cli/src/perform-compile.ts index 55feddb347..b7efcb2bf9 100644 --- a/packages/compiler-cli/src/perform-compile.ts +++ b/packages/compiler-cli/src/perform-compile.ts @@ -7,10 +7,11 @@ */ import {isSyntaxError, syntaxError} from '@angular/compiler'; -import {MetadataBundler, createBundleIndexHost} from '@angular/tsc-wrapped'; +import {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'; @@ -70,17 +71,6 @@ export function throwOnDiagnostics(cwd: string, ...args: Diagnostics[]) { } } -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, @@ -111,28 +101,22 @@ export function readConfiguration( 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; -} - +/** + * Returns an object with two properties: + * - `errorCode` is 0 when the compilation was successful, + * - `result` is an `EmitResult` when the errorCode is 0, `undefined` otherwise. + */ export function performCompilation( - basePath: string, files: string[], options: ts.CompilerOptions, ngOptions: any, + basePath: string, files: string[], options: ts.CompilerOptions, ngOptions: api.CompilerOptions, consoleError: (s: string) => void = console.error, checkFunc: (cwd: string, ...args: any[]) => void = throwOnDiagnostics, - tsCompilerHost?: ts.CompilerHost) { + tsCompilerHost?: ts.CompilerHost): {errorCode: number, result?: api.EmitResult} { + const [major, minor] = ts.version.split('.'); + + if (+major < 2 || (+major === 2 && +minor < 3)) { + throw new Error('Must use TypeScript > 2.3 to have transformer support'); + } + try { ngOptions.basePath = basePath; ngOptions.genDir = basePath; @@ -175,18 +159,20 @@ export function performCompilation( // Check Angular semantic diagnostics checkFunc(basePath, ngProgram.getNgSemanticDiagnostics()); - ngProgram.emit({ + const result = ngProgram.emit({ emitFlags: api.EmitFlags.Default | ((ngOptions.skipMetadataEmit || ngOptions.flatModuleOutFile) ? 0 : api.EmitFlags.Metadata) }); + + checkFunc(basePath, result.diagnostics); + + return {errorCode: 0, result}; } catch (e) { if (isSyntaxError(e)) { - console.error(e.message); consoleError(e.message); - return 1; + return {errorCode: 1}; } + throw e; } - - return 0; -} +} \ No newline at end of file diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index de1ccc3b9d..c48c67327b 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -24,6 +24,7 @@ export interface Diagnostic { export interface CompilerOptions extends ts.CompilerOptions { // Absolute path to a directory where generated file structure is written. // If unspecified, generated files will be written alongside sources. + // @deprecated - no effect genDir?: string; // Path to the directory containing the tsconfig.json file. @@ -95,6 +96,27 @@ export interface CompilerOptions extends ts.CompilerOptions { // Whether to enable lowering expressions lambdas and expressions in a reference value // position. disableExpressionLowering?: boolean; + + // The list of expected files, when provided: + // - extra files are filtered out, + // - missing files are created empty. + expectedOut?: string[]; + + // Locale of the application + i18nOutLocale?: string; + // Export format (xlf, xlf2 or xmb) + i18nOutFormat?: string; + // Path to the extracted message file + i18nOutFile?: string; + + // Import format if different from `i18nFormat` + i18nInFormat?: string; + // Locale of the imported translations + i18nInLocale?: string; + // Path to the translation file + i18nInFile?: string; + // How to handle missing messages + i18nInMissingTranslations?: 'error'|'warning'|'ignore'; } export interface ModuleFilenameResolver { @@ -146,6 +168,11 @@ export enum EmitFlags { // afterTs?: ts.TransformerFactory[]; // } +export interface EmitResult extends ts.EmitResult { + modulesManifest: {modules: string[]; fileNames: string[];}; + externs: {[fileName: string]: string;}; +} + export interface Program { /** * Retrieve the TypeScript program used to produce semantic diagnostics and emit the sources. @@ -155,7 +182,7 @@ export interface Program { getTsProgram(): ts.Program; /** - * Retreive options diagnostics for the TypeScript options used to create the program. This is + * Retrieve options diagnostics for the TypeScript options used to create the program. This is * faster than calling `getTsProgram().getOptionsDiagnostics()` since it does not need to * collect Angular structural information to produce the errors. */ @@ -167,7 +194,7 @@ export interface Program { getNgOptionDiagnostics(cancellationToken?: ts.CancellationToken): Diagnostic[]; /** - * Retrive the syntax diagnostics from TypeScript. This is faster than calling + * Retrieve the syntax diagnostics from TypeScript. This is faster than calling * `getTsProgram().getSyntacticDiagnostics()` since it does not need to collect Angular structural * information to produce the errors. */ @@ -188,7 +215,7 @@ export interface Program { getNgStructuralDiagnostics(cancellationToken?: ts.CancellationToken): Diagnostic[]; /** - * Retreive the semantic diagnostics from TypeScript. This is equivilent to calling + * Retrieve the semantic diagnostics from TypeScript. This is equivilent to calling * `getTsProgram().getSemanticDiagnostics()` directly and is included for completeness. */ getTsSemanticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): @@ -227,5 +254,5 @@ export interface Program { emitFlags: EmitFlags, // transformers?: CustomTransformers, // See TODO above cancellationToken?: ts.CancellationToken, - }): void; + }): EmitResult; } diff --git a/packages/compiler-cli/src/transformers/lower_expressions.ts b/packages/compiler-cli/src/transformers/lower_expressions.ts index 65c4cd3aff..1a2edb825e 100644 --- a/packages/compiler-cli/src/transformers/lower_expressions.ts +++ b/packages/compiler-cli/src/transformers/lower_expressions.ts @@ -55,19 +55,26 @@ function transformSourceFile( context: ts.TransformationContext): ts.SourceFile { const inserts: DeclarationInsert[] = []; - // Calculate the range of intersting locations. The transform will only visit nodes in this + // Calculate the range of interesting locations. The transform will only visit nodes in this // range to improve the performance on large files. const locations = Array.from(requests.keys()); const min = Math.min(...locations); const max = Math.max(...locations); + // Visit nodes matching the request and synthetic nodes added by tsickle + function shouldVisit(pos: number, end: number): boolean { + return (pos <= max && end >= min) || pos == -1; + } + function visitSourceFile(sourceFile: ts.SourceFile): ts.SourceFile { function topLevelStatement(node: ts.Node): ts.Node { const declarations: Declaration[] = []; function visitNode(node: ts.Node): ts.Node { - const nodeRequest = requests.get(node.pos); - if (nodeRequest && nodeRequest.kind == node.kind && nodeRequest.end == node.end) { + // Get the original node before tsickle + const {pos, end, kind} = ts.getOriginalNode(node); + const nodeRequest = requests.get(pos); + if (nodeRequest && nodeRequest.kind == kind && nodeRequest.end == end) { // This node is requested to be rewritten as a reference to the exported name. // Record that the node needs to be moved to an exported variable with the given name const name = nodeRequest.name; @@ -75,14 +82,16 @@ function transformSourceFile( return ts.createIdentifier(name); } let result = node; - if (node.pos <= max && node.end >= min && !isLexicalScope(node)) { + + if (shouldVisit(pos, end) && !isLexicalScope(node)) { result = ts.visitEachChild(node, visitNode, context); } return result; } - const result = - (node.pos <= max && node.end >= min) ? ts.visitEachChild(node, visitNode, context) : node; + // Get the original node before tsickle + const {pos, end} = ts.getOriginalNode(node); + const result = shouldVisit(pos, end) ? ts.visitEachChild(node, visitNode, context) : node; if (declarations.length) { inserts.push({priorTo: result, declarations}); @@ -91,6 +100,7 @@ function transformSourceFile( } const traversedSource = ts.visitEachChild(sourceFile, topLevelStatement, context); + if (inserts.length) { // Insert the declarations before the rewritten statement that references them. const insertMap = toMap(inserts, i => i.priorTo); diff --git a/packages/compiler-cli/src/transformers/node_emitter.ts b/packages/compiler-cli/src/transformers/node_emitter.ts index 8adc7e8f64..29cd86863c 100644 --- a/packages/compiler-cli/src/transformers/node_emitter.ts +++ b/packages/compiler-cli/src/transformers/node_emitter.ts @@ -19,8 +19,10 @@ export class TypeScriptNodeEmitter { updateSourceFile(sourceFile: ts.SourceFile, stmts: Statement[], preamble?: string): [ts.SourceFile, Map] { const converter = new _NodeEmitterVisitor(); - const statements = - stmts.map(stmt => stmt.visitStatement(converter, null)).filter(stmt => stmt != null); + // [].concat flattens the result so that each `visit...` method can also return an array of + // stmts. + const statements: any[] = [].concat( + ...stmts.map(stmt => stmt.visitStatement(converter, null)).filter(stmt => stmt != null)); const newSourceFile = ts.updateSourceFileNode( sourceFile, [...converter.getReexports(), ...converter.getImports(), ...statements]); if (preamble) { @@ -118,20 +120,30 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor { } } - return this.record( - stmt, ts.createVariableStatement( - this.getModifiers(stmt), - ts.createVariableDeclarationList([ts.createVariableDeclaration( - ts.createIdentifier(stmt.name), - /* type */ undefined, - (stmt.value && stmt.value.visitExpression(this, null)) || undefined)]))); + const varDeclList = ts.createVariableDeclarationList([ts.createVariableDeclaration( + ts.createIdentifier(stmt.name), + /* type */ undefined, + (stmt.value && stmt.value.visitExpression(this, null)) || undefined)]); + + if (stmt.hasModifier(StmtModifier.Exported)) { + // Note: We need to add an explicit variable and export declaration so that + // the variable can be referred in the same file as well. + const tsVarStmt = + this.record(stmt, ts.createVariableStatement(/* modifiers */[], varDeclList)); + const exportStmt = this.record( + stmt, ts.createExportDeclaration( + /*decorators*/ undefined, /*modifiers*/ undefined, + ts.createNamedExports([ts.createExportSpecifier(stmt.name, stmt.name)]))); + return [tsVarStmt, exportStmt]; + } + return this.record(stmt, ts.createVariableStatement(this.getModifiers(stmt), varDeclList)); } visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any) { return this.record( stmt, ts.createFunctionDeclaration( /* decorators */ undefined, this.getModifiers(stmt), - /* astrictToken */ undefined, stmt.name, /* typeParameters */ undefined, + /* asteriskToken */ undefined, stmt.name, /* typeParameters */ undefined, stmt.params.map( p => ts.createParameter( /* decorators */ undefined, /* modifiers */ undefined, diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index 3970aeb015..c1c0fe2da6 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -6,20 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotCompiler, GeneratedFile, NgAnalyzedModules, createAotCompiler, getParseErrors, isSyntaxError, toTypeScript} from '@angular/compiler'; -import {MetadataCollector, ModuleMetadata} from '@angular/tsc-wrapped'; -import {writeFileSync} from 'fs'; +import {AotCompiler, AotCompilerOptions, GeneratedFile, NgAnalyzedModules, createAotCompiler, getParseErrors, isSyntaxError, toTypeScript} from '@angular/compiler'; +import {MissingTranslationStrategy} from '@angular/core'; +import * as fs from 'fs'; import * as path from 'path'; +import * as tsickle from 'tsickle'; import * as ts from 'typescript'; -import {CompilerHost as AotCompilerHost, CompilerHostContext} from '../compiler_host'; +import {CompilerHost as AotCompilerHost} from '../compiler_host'; import {TypeChecker} from '../diagnostics/check_types'; -import {CompilerHost, CompilerOptions, Diagnostic, DiagnosticCategory, EmitFlags, Program} from './api'; +import {CompilerHost, CompilerOptions, Diagnostic, DiagnosticCategory, EmitFlags, EmitResult, Program} from './api'; import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions'; import {getAngularEmitterTransformFactory} from './node_emitter_transform'; const GENERATED_FILES = /\.ngfactory\.js$|\.ngstyle\.js$|\.ngsummary\.js$/; + const SUMMARY_JSON_FILES = /\.ngsummary.json$/; const emptyModules: NgAnalyzedModules = { @@ -52,17 +54,20 @@ class AngularCompilerProgram implements Program { private rootNames: string[], private options: CompilerOptions, private host: CompilerHost, private oldProgram?: Program) { this.oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined; - this.tsProgram = ts.createProgram(rootNames, options, host, this.oldTsProgram); - this.srcNames = this.tsProgram.getSourceFiles().map(sf => sf.fileName); + this.srcNames = + this.tsProgram.getSourceFiles() + .map(sf => sf.fileName) + .filter(f => !f.match(/\.ngfactory\.[\w.]+$|\.ngstyle\.[\w.]+$|\.ngsummary\.[\w.]+$/)); this.metadataCache = new LowerMetadataCache({quotedNames: true}, !!options.strictMetadataEmit); this.aotCompilerHost = new AotCompilerHost( this.tsProgram, options, host, /* collectorOptions */ undefined, this.metadataCache); if (host.readResource) { this.aotCompilerHost.loadResource = host.readResource.bind(host); } - const {compiler} = createAotCompiler(this.aotCompilerHost, options); - this.compiler = compiler; + + const aotOptions = getAotCompilerOptions(options); + this.compiler = createAotCompiler(this.aotCompilerHost, aotOptions).compiler; } // Program implementation @@ -115,25 +120,56 @@ class AngularCompilerProgram implements Program { getLazyRoutes(cancellationToken?: ts.CancellationToken): {[route: string]: string} { return {}; } emit({emitFlags = EmitFlags.Default, cancellationToken}: - {emitFlags?: EmitFlags, cancellationToken?: ts.CancellationToken}): ts.EmitResult { + {emitFlags?: EmitFlags, cancellationToken?: ts.CancellationToken}): EmitResult { const emitMap = new Map(); - const result = this.programWithStubs.emit( + + const tsickleCompilerHostOptions: tsickle.TransformerOptions = { + googmodule: false, + untyped: true, + convertIndexImportShorthand: true, + transformDecorators: this.options.annotationsAs !== 'decorators', + transformTypesToClosure: this.options.annotateForClosureCompiler, + }; + + const tsickleHost: tsickle.TransformerHost = { + shouldSkipTsickleProcessing: (fileName) => /\.d\.ts$/.test(fileName), + pathToModuleName: (context, importPath) => '', + shouldIgnoreWarningsForPath: (filePath) => false, + fileNameToModuleId: (fileName) => fileName, + }; + + const expectedOut = this.options.expectedOut ? + this.options.expectedOut.map(f => path.resolve(process.cwd(), f)) : + undefined; + + const result = tsickle.emitWithTsickle( + this.programWithStubs, tsickleHost, tsickleCompilerHostOptions, this.host, this.options, /* targetSourceFile */ undefined, - createWriteFileCallback(emitFlags, this.host, this.metadataCache, emitMap), + createWriteFileCallback(emitFlags, this.host, this.metadataCache, emitMap, expectedOut), cancellationToken, (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS, this.calculateTransforms()); this.generatedFiles.forEach(file => { + // In order not to replicate the TS calculation of the out folder for files + // derive the out location for .json files from the out location of the .ts files if (file.source && file.source.length && SUMMARY_JSON_FILES.test(file.genFileUrl)) { // If we have emitted the ngsummary.ts file, ensure the ngsummary.json file is emitted to // the same location. + const emittedFile = emitMap.get(file.srcFileUrl); - const fileName = emittedFile ? - path.join(path.dirname(emittedFile), path.basename(file.genFileUrl)) : - file.genFileUrl; - this.host.writeFile(fileName, file.source, false, error => {}); + + if (emittedFile) { + const fileName = path.join(path.dirname(emittedFile), path.basename(file.genFileUrl)); + this.host.writeFile(fileName, file.source, false, error => {}); + } } }); + + // Ensure that expected output files exist. + for (const out of expectedOut || []) { + fs.appendFileSync(out, '', 'utf8'); + } + return result; } @@ -183,19 +219,15 @@ class AngularCompilerProgram implements Program { return this.generatedFiles && this._generatedFileDiagnostics !; } - private calculateTransforms(): ts.CustomTransformers { - const before: ts.TransformerFactory[] = []; - const after: ts.TransformerFactory[] = []; + private calculateTransforms(): tsickle.EmitTransformers { + const beforeTs: ts.TransformerFactory[] = []; if (!this.options.disableExpressionLowering) { - before.push(getExpressionLoweringTransformFactory(this.metadataCache)); + beforeTs.push(getExpressionLoweringTransformFactory(this.metadataCache)); } if (!this.options.skipTemplateCodegen) { - after.push(getAngularEmitterTransformFactory(this.generatedFiles)); + beforeTs.push(getAngularEmitterTransformFactory(this.generatedFiles)); } - const result: ts.CustomTransformers = {}; - if (before.length) result.before = before; - if (after.length) result.after = after; - return result; + return {beforeTs}; } private catchAnalysisError(e: any): NgAnalyzedModules { @@ -228,8 +260,8 @@ class AngularCompilerProgram implements Program { private generateStubs() { return this.options.skipTemplateCodegen ? [] : this.options.generateCodeForLibraries === false ? - this.compiler.emitAllStubs(this.analyzedModules) : - this.compiler.emitPartialStubs(this.analyzedModules); + this.compiler.emitPartialStubs(this.analyzedModules) : + this.compiler.emitAllStubs(this.analyzedModules); } private generateFiles() { @@ -270,6 +302,40 @@ export function createProgram( return new AngularCompilerProgram(rootNames, options, host, oldProgram); } +// Compute the AotCompiler options +function getAotCompilerOptions(options: CompilerOptions): AotCompilerOptions { + let missingTranslation = MissingTranslationStrategy.Warning; + + switch (options.i18nInMissingTranslations) { + case 'ignore': + missingTranslation = MissingTranslationStrategy.Ignore; + break; + case 'error': + missingTranslation = MissingTranslationStrategy.Error; + break; + } + + let translations: string = ''; + + if (options.i18nInFile) { + if (!options.locale) { + throw new Error(`The translation file (${options.i18nInFile}) locale must be provided.`); + } + translations = fs.readFileSync(options.i18nInFile, 'utf8'); + } else { + // No translations are provided, ignore any errors + // We still go through i18n to remove i18n attributes + missingTranslation = MissingTranslationStrategy.Ignore; + } + + return { + locale: options.i18nInLocale, + i18nFormat: options.i18nInFormat || options.i18nOutFormat, translations, missingTranslation, + enableLegacyTemplate: options.enableLegacyTemplate, + enableSummariesForJit: true, + }; +} + function writeMetadata( emitFilePath: string, sourceFile: ts.SourceFile, metadataCache: LowerMetadataCache) { if (/\.js$/.test(emitFilePath)) { @@ -287,38 +353,36 @@ function writeMetadata( const metadata = metadataCache.getMetadata(collectableFile); if (metadata) { const metadataText = JSON.stringify([metadata]); - writeFileSync(path, metadataText, {encoding: 'utf-8'}); + fs.writeFileSync(path, metadataText, {encoding: 'utf-8'}); } } } function createWriteFileCallback( emitFlags: EmitFlags, host: ts.CompilerHost, metadataCache: LowerMetadataCache, - emitMap: Map) { - const withMetadata = - (fileName: string, data: string, writeByteOrderMark: boolean, - onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => { - const generatedFile = GENERATED_FILES.test(fileName); - if (!generatedFile || data != '') { - host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); - } - if (!generatedFile && sourceFiles && sourceFiles.length == 1) { - emitMap.set(sourceFiles[0].fileName, fileName); - writeMetadata(fileName, sourceFiles[0], metadataCache); - } - }; - const withoutMetadata = - (fileName: string, data: string, writeByteOrderMark: boolean, - onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => { - const generatedFile = GENERATED_FILES.test(fileName); - if (!generatedFile || data != '') { - host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); - } - if (!generatedFile && sourceFiles && sourceFiles.length == 1) { - emitMap.set(sourceFiles[0].fileName, fileName); - } - }; - return (emitFlags & EmitFlags.Metadata) != 0 ? withMetadata : withoutMetadata; + emitMap: Map, expectedOut?: string[]) { + return (fileName: string, data: string, writeByteOrderMark: boolean, + onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => { + + let srcFile: ts.SourceFile|undefined; + + if (sourceFiles && sourceFiles.length == 1) { + srcFile = sourceFiles[0]; + emitMap.set(srcFile.fileName, fileName); + } + + const absFile = path.resolve(process.cwd(), fileName); + const generatedFile = GENERATED_FILES.test(fileName); + + // Don't emit unexpected files nor empty generated files + if ((!expectedOut || expectedOut.indexOf(absFile) > -1) && (!generatedFile || data)) { + host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); + + if (srcFile && !generatedFile && (emitFlags & EmitFlags.Metadata) != 0) { + writeMetadata(fileName, srcFile, metadataCache); + } + } + }; } function getNgOptionDiagnostics(options: CompilerOptions): Diagnostic[] { diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index 53320cc186..165582a09c 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -82,19 +82,19 @@ describe('ngc command-line', () => { spyOn(mockConsole, 'error'); - const result = performCompilation( - basePath, [path.join(basePath, 'test.ts')], { - experimentalDecorators: true, - skipLibCheck: true, - types: [], - outDir: path.join(basePath, 'built'), - declaration: true, - module: ts.ModuleKind.ES2015, - moduleResolution: ts.ModuleResolutionKind.NodeJs, - }, - {}, mockConsole.error); - expect(mockConsole.error).not.toHaveBeenCalled(); - expect(result).toBe(0); + expect( + () => performCompilation( + basePath, [path.join(basePath, 'test.ts')], { + experimentalDecorators: true, + skipLibCheck: true, + types: [], + outDir: path.join(basePath, 'built'), + declaration: true, + module: ts.ModuleKind.ES2015, + moduleResolution: ts.ModuleResolutionKind.NodeJs, + }, + {})) + .not.toThrow(); }); it('should not print the stack trace if user input file does not exist', () => { @@ -292,7 +292,7 @@ describe('ngc command-line', () => { .toBe(true); }); - it('should compile with a explicit tsconfig reference', () => { + it('should compile with an explicit tsconfig reference', () => { writeConfig(`{ "extends": "./tsconfig-base.json", "files": ["mymodule.ts"] @@ -316,6 +316,92 @@ describe('ngc command-line', () => { .toBe(true); }); + describe('closure', () => { + it('should not generate closure specific code by default', () => { + writeConfig(`{ + "extends": "./tsconfig-base.json", + "files": ["mymodule.ts"] + }`); + write('mymodule.ts', ` + import {NgModule, Component} from '@angular/core'; + + @Component({template: ''}) + export class MyComp {} + + @NgModule({declarations: [MyComp]}) + export class MyModule {} + `); + + const mockConsole = {error: (s: string) => {}}; + const exitCode = main(['-p', basePath], mockConsole.error); + expect(exitCode).toEqual(0); + + const mymodulejs = path.resolve(outDir, 'mymodule.js'); + const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8'); + expect(mymoduleSource).not.toContain('@fileoverview added by tsickle'); + expect(mymoduleSource).toContain('MyComp.decorators = ['); + }); + + it('should add closure annotations', () => { + writeConfig(`{ + "extends": "./tsconfig-base.json", + "angularCompilerOptions": { + "annotateForClosureCompiler": true + }, + "files": ["mymodule.ts"] + }`); + write('mymodule.ts', ` + import {NgModule, Component} from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + fn(p: any) {} + } + + @NgModule({declarations: [MyComp]}) + export class MyModule {} + `); + + const mockConsole = {error: (s: string) => {}}; + const exitCode = main(['-p', basePath], mockConsole.error); + expect(exitCode).toEqual(0); + + const mymodulejs = path.resolve(outDir, 'mymodule.js'); + const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8'); + expect(mymoduleSource).toContain('@fileoverview added by tsickle'); + expect(mymoduleSource).toContain('@param {?} p'); + }); + + it('should add metadata as decorators', () => { + writeConfig(`{ + "extends": "./tsconfig-base.json", + "angularCompilerOptions": { + "annotationsAs": "decorators" + }, + "files": ["mymodule.ts"] + }`); + write('mymodule.ts', ` + import {NgModule, Component} from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + fn(p: any) {} + } + + @NgModule({declarations: [MyComp]}) + export class MyModule {} + `); + + const mockConsole = {error: (s: string) => {}}; + const exitCode = main(['-p', basePath], mockConsole.error); + expect(exitCode).toEqual(0); + + const mymodulejs = path.resolve(outDir, 'mymodule.js'); + const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8'); + expect(mymoduleSource).toContain('MyComp = __decorate(['); + }); + }); + describe('expression lowering', () => { beforeEach(() => { writeConfig(`{ @@ -437,7 +523,7 @@ describe('ngc command-line', () => { import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; - class Foo {} + export class Foo {} export const factory = () => new Foo(); @@ -557,7 +643,7 @@ describe('ngc command-line', () => { export class FlatModule { }`); - const exitCode = performCompilation( + const emitResult = performCompilation( basePath, [path.join(basePath, 'public-api.ts')], { target: ts.ScriptTarget.ES5, experimentalDecorators: true, @@ -578,7 +664,7 @@ describe('ngc command-line', () => { }); - expect(exitCode).toEqual(0); + expect(emitResult.errorCode).toEqual(0); shouldExist('index.js'); shouldExist('index.metadata.json'); }); @@ -758,7 +844,7 @@ describe('ngc command-line', () => { write(path.join(dir, 'tsconfig.json'), ` { "angularCompilerOptions": { - "generateCodeForLibraries": false, + "generateCodeForLibraries": true, "enableSummariesForJit": true }, "compilerOptions": { @@ -820,7 +906,7 @@ describe('ngc command-line', () => { shouldExist('lib1/module.ngfactory.d.ts'); }); - it('should be able to compiler library 2', () => { + it('should be able to compile library 2', () => { expect(main(['-p', path.join(basePath, 'lib1')])).toBe(0); expect(main(['-p', path.join(basePath, 'lib2')])).toBe(0); shouldExist('lib2/module.js'); diff --git a/packages/compiler-cli/test/transformers/node_emitter_spec.ts b/packages/compiler-cli/test/transformers/node_emitter_spec.ts index 2918e70535..f7b169c2a7 100644 --- a/packages/compiler-cli/test/transformers/node_emitter_spec.ts +++ b/packages/compiler-cli/test/transformers/node_emitter_spec.ts @@ -63,7 +63,7 @@ describe('TypeScriptNodeEmitter', () => { expect(emitStmt(someVar.set(o.literal(1)).toDeclStmt(null, [o.StmtModifier.Final]))) .toEqual(`var someVar = 1;`); expect(emitStmt(someVar.set(o.literal(1)).toDeclStmt(null, [o.StmtModifier.Exported]))) - .toEqual(`exports.someVar = 1;`); + .toEqual(`var someVar = 1; exports.someVar = someVar;`); }); describe('declare variables with ExternExpressions as values', () => { @@ -71,7 +71,7 @@ describe('TypeScriptNodeEmitter', () => { // identifier is in the same module -> no reexport expect(emitStmt(someVar.set(o.importExpr(sameModuleIdentifier)).toDeclStmt(null, [ o.StmtModifier.Exported - ]))).toEqual('exports.someVar = someLocalId;'); + ]))).toEqual('var someVar = someLocalId; exports.someVar = someVar;'); }); it('should create no reexport if the variable is not exported', () => { @@ -84,7 +84,7 @@ describe('TypeScriptNodeEmitter', () => { expect(emitStmt(someVar.set(o.importExpr(externalModuleIdentifier)) .toDeclStmt(o.DYNAMIC_TYPE, [o.StmtModifier.Exported]))) .toEqual( - `const i0 = require("/somePackage/someOtherPath"); exports.someVar = i0.someExternalId;`); + `const i0 = require("/somePackage/someOtherPath"); var someVar = i0.someExternalId; exports.someVar = someVar;`); }); it('should create a reexport', () => { diff --git a/packages/platform-server/integrationtest/package.json b/packages/platform-server/integrationtest/package.json index 19c499fa9f..0016512b7b 100644 --- a/packages/platform-server/integrationtest/package.json +++ b/packages/platform-server/integrationtest/package.json @@ -18,7 +18,7 @@ "@angular/platform-server": "file:../../../dist/packages-dist/platform-server", "express": "^4.14.1", "rxjs": "file:../../../node_modules/rxjs", - "typescript": "2.1.6", + "typescript": "2.3.x", "zone.js": "^0.8.10" }, "devDependencies": { diff --git a/scripts/ci/offline_compiler_test.sh b/scripts/ci/offline_compiler_test.sh index f419884f85..b8a0703557 100755 --- a/scripts/ci/offline_compiler_test.sh +++ b/scripts/ci/offline_compiler_test.sh @@ -8,7 +8,7 @@ LINKABLE_PKGS=( $(pwd)/dist/packages-dist/{common,forms,core,compiler,compiler-cli,platform-{browser,server},platform-browser-dynamic,router,http,animations,tsc-wrapped} ) -TYPESCRIPT_2_1=typescript@2.1.5 +TYPESCRIPT_2_3=typescript@2.3.x PKGS=( reflect-metadata@0.1.8 zone.js@0.6.25 @@ -33,7 +33,7 @@ cp -v package.json $TMP ( cd $TMP set -ex -o pipefail - npm install ${PKGS[*]} $TYPESCRIPT_2_1 + npm install ${PKGS[*]} $TYPESCRIPT_2_3 # TODO(alexeagle): allow this to be npm link instead npm install ${LINKABLE_PKGS[*]} @@ -54,7 +54,6 @@ cp -v package.json $TMP # Copy the html files from source to the emitted output cp flat_module/src/*.html node_modules/flat_module/src - ./node_modules/.bin/ngc -p tsconfig-build-alt.json --missingTranslation=error --i18nFormat=xlf ./node_modules/.bin/ngc -p tsconfig-build.json --i18nFile=src/messages.fi.xlf --locale=fi --i18nFormat=xlf ./node_modules/.bin/ng-xi18n -p tsconfig-xi18n.json --i18nFormat=xlf --locale=fr diff --git a/tools/ngc-wrapped/index.ts b/tools/ngc-wrapped/index.ts index 37d88317dd..0d32fce43b 100644 --- a/tools/ngc-wrapped/index.ts +++ b/tools/ngc-wrapped/index.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -// TODO(chuckj): Remove the requirment for a fake 'reflect` implementation from +// TODO(chuckj): Remove the requirement for a fake 'reflect` implementation from // the compiler import 'reflect-metadata'; import {performCompilation} from '@angular/compiler-cli'; @@ -28,16 +28,7 @@ function main(args: string[]) { const basePath = path.resolve(process.cwd(), projectDir); const result = performCompilation(basePath, files, options, ngOptions, undefined); - if (result === 0) { - // Ensure that expected output files exist. - if (ngOptions && ngOptions.expectedOut) { - for (const out of ngOptions.expectedOut) { - fs.appendFileSync(out, '', 'utf-8'); - } - } - } - - return result; + return result.errorCode; } if (require.main === module) { From cac130eff9b9cb608f2308ae40c42c9cd1850c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mis=CC=8Cko=20Hevery?= Date: Tue, 8 Aug 2017 14:03:27 -0700 Subject: [PATCH 17/83] perf(core): Remove decorator DSL which depends on Reflect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE It is no longer possible to declare classes in this format. ``` Component({...}). Class({ constructor: function() {...} }) ``` This format would only work with JIT and with ES5. This mode doesn’t allow build tools like Webpack to process and optimize the code, which results in prohibitively large bundles. We are removing this API because we are trying to ensure that everyone is on the fast path by default, and it is not possible to get on the fast path using the ES5 DSL. The replacement is to use TypeScript and `@Decorator` format. ``` @Component({...}) class { constructor() {...} } ``` --- packages/core/src/core.ts | 2 +- packages/core/src/di/reflective_injector.ts | 5 +- packages/core/src/metadata/di.ts | 12 - .../platform_reflection_capabilities.ts | 12 + .../src/reflection/reflection_capabilities.ts | 22 +- packages/core/src/util/decorators.ts | 261 +------ .../core/test/metadata/decorators_spec.ts | 32 - packages/core/test/util/decorators_spec.ts | 101 +-- .../upgrade/src/dynamic/upgrade_adapter.ts | 26 +- .../src/dynamic/upgrade_ng1_adapter.ts | 37 +- packages/upgrade/test/dynamic/upgrade_spec.ts | 688 ++++++++++-------- tools/public_api_guard/core/core.d.ts | 13 - 12 files changed, 456 insertions(+), 755 deletions(-) delete mode 100644 packages/core/test/metadata/decorators_spec.ts diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index 772da782a8..2ce897f2de 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -13,7 +13,7 @@ */ export * from './metadata'; export * from './version'; -export {Class, ClassDefinition, TypeDecorator} from './util/decorators'; +export {TypeDecorator} from './util/decorators'; export * from './di'; export {createPlatform, assertPlatform, destroyPlatform, getPlatform, PlatformRef, ApplicationRef, enableProdMode, isDevMode, createPlatformFactory, NgProbeToken} from './application_ref'; export {APP_ID, PACKAGE_ROOT_URL, PLATFORM_INITIALIZER, PLATFORM_ID, APP_BOOTSTRAP_LISTENER} from './application_tokens'; diff --git a/packages/core/src/di/reflective_injector.ts b/packages/core/src/di/reflective_injector.ts index 5e7e0a481c..6b7f4c47d0 100644 --- a/packages/core/src/di/reflective_injector.ts +++ b/packages/core/src/di/reflective_injector.ts @@ -277,6 +277,7 @@ export abstract class ReflectiveInjector implements Injector { } export class ReflectiveInjector_ implements ReflectiveInjector { + private static INJECTOR_KEY = ReflectiveKey.get(Injector); /** @internal */ _constructionCounter: number = 0; /** @internal */ @@ -389,7 +390,7 @@ export class ReflectiveInjector_ implements ReflectiveInjector { } private _getByKey(key: ReflectiveKey, visibility: Self|SkipSelf|null, notFoundValue: any): any { - if (key === INJECTOR_KEY) { + if (key === ReflectiveInjector_.INJECTOR_KEY) { return this; } @@ -463,8 +464,6 @@ export class ReflectiveInjector_ implements ReflectiveInjector { toString(): string { return this.displayName; } } -const INJECTOR_KEY = ReflectiveKey.get(Injector); - function _mapProviders(injector: ReflectiveInjector_, fn: Function): any[] { const res: any[] = new Array(injector._providers.length); for (let i = 0; i < injector._providers.length; ++i) { diff --git a/packages/core/src/metadata/di.ts b/packages/core/src/metadata/di.ts index 5efabd0033..e12522be12 100644 --- a/packages/core/src/metadata/di.ts +++ b/packages/core/src/metadata/di.ts @@ -73,18 +73,6 @@ export interface AttributeDecorator { * * {@example core/ts/metadata/metadata.ts region='attributeFactory'} * - * ### Example as ES5 DSL - * - * ``` - * var MyComponent = ng - * .Component({...}) - * .Class({ - * constructor: [new ng.Attribute('title'), function(title) { - * ... - * }] - * }) - * ``` - * * ### Example as ES5 annotation * * ``` diff --git a/packages/core/src/reflection/platform_reflection_capabilities.ts b/packages/core/src/reflection/platform_reflection_capabilities.ts index 22fa0cd656..8cc5b7862b 100644 --- a/packages/core/src/reflection/platform_reflection_capabilities.ts +++ b/packages/core/src/reflection/platform_reflection_capabilities.ts @@ -13,8 +13,20 @@ export interface PlatformReflectionCapabilities { isReflectionEnabled(): boolean; factory(type: Type): Function; hasLifecycleHook(type: any, lcProperty: string): boolean; + + /** + * Return a list of annotations/types for constructor parameters + */ parameters(type: Type): any[][]; + + /** + * Return a list of annotations declared on the class + */ annotations(type: Type): any[]; + + /** + * Return a object literal which describes the annotations on Class fields/properties. + */ propMetadata(typeOrFunc: Type): {[key: string]: any[]}; getter(name: string): GetterFn; setter(name: string): SetterFn; diff --git a/packages/core/src/reflection/reflection_capabilities.ts b/packages/core/src/reflection/reflection_capabilities.ts index b7aa346ebf..4f74baf886 100644 --- a/packages/core/src/reflection/reflection_capabilities.ts +++ b/packages/core/src/reflection/reflection_capabilities.ts @@ -8,9 +8,12 @@ import {Type, isType} from '../type'; import {global, stringify} from '../util'; +import {ANNOTATIONS, PARAMETERS, PROP_METADATA} from '../util/decorators'; + import {PlatformReflectionCapabilities} from './platform_reflection_capabilities'; import {GetterFn, MethodFn, SetterFn} from './types'; + /** * Attention: This regex has to hold even if the code is minified! */ @@ -85,12 +88,11 @@ export class ReflectionCapabilities implements PlatformReflectionCapabilities { } // API for metadata created by invoking the decorators. - if (this._reflect != null && this._reflect.getOwnMetadata != null) { - const paramAnnotations = this._reflect.getOwnMetadata('parameters', type); - const paramTypes = this._reflect.getOwnMetadata('design:paramtypes', type); - if (paramTypes || paramAnnotations) { - return this._zipTypesAndAnnotations(paramTypes, paramAnnotations); - } + const paramAnnotations = type.hasOwnProperty(PARAMETERS) && (type as any)[PARAMETERS]; + const paramTypes = this._reflect && this._reflect.getOwnMetadata && + this._reflect.getOwnMetadata('design:paramtypes', type); + if (paramTypes || paramAnnotations) { + return this._zipTypesAndAnnotations(paramTypes, paramAnnotations); } // If a class has no decorators, at least create metadata @@ -130,8 +132,8 @@ export class ReflectionCapabilities implements PlatformReflectionCapabilities { } // API for metadata created by invoking the decorators. - if (this._reflect && this._reflect.getOwnMetadata) { - return this._reflect.getOwnMetadata('annotations', typeOrFunc); + if (typeOrFunc.hasOwnProperty(ANNOTATIONS)) { + return (typeOrFunc as any)[ANNOTATIONS]; } return null; } @@ -169,8 +171,8 @@ export class ReflectionCapabilities implements PlatformReflectionCapabilities { } // API for metadata created by invoking the decorators. - if (this._reflect && this._reflect.getOwnMetadata) { - return this._reflect.getOwnMetadata('propMetadata', typeOrFunc); + if (typeOrFunc.hasOwnProperty(PROP_METADATA)) { + return (typeOrFunc as any)[PROP_METADATA]; } return null; } diff --git a/packages/core/src/util/decorators.ts b/packages/core/src/util/decorators.ts index 1acf99b60b..2aae8c6a1d 100644 --- a/packages/core/src/util/decorators.ts +++ b/packages/core/src/util/decorators.ts @@ -7,54 +7,12 @@ */ import {Type} from '../type'; -import {global, stringify} from '../util'; - -let _nextClassId = 0; -const Reflect = global['Reflect']; - -/** - * Declares the interface to be used with {@link Class}. - * - * @stable - */ -export type ClassDefinition = { - /** - * Optional argument for specifying the superclass. - */ - extends?: Type; - - /** - * Required constructor function for a class. - * - * The function may be optionally wrapped in an `Array`, in which case additional parameter - * annotations may be specified. - * The number of arguments and the number of parameter annotations must match. - * - * See {@link Class} for example of usage. - */ - constructor: Function | any[]; -} & -{ - /** - * Other methods on the class. Note that values should have type 'Function' but TS requires - * all properties to have a narrower type than the index signature. - */ - [x: string]: Type|Function|any[]; -}; /** * An interface implemented by all Angular type decorators, which allows them to be used as ES7 * decorators as well as * Angular DSL syntax. * - * DSL syntax: - * - * ``` - * var MyClass = ng - * .Component({...}) - * .Class({...}); - * ``` - * * ES7 syntax: * * ``` @@ -74,189 +32,11 @@ export interface TypeDecorator { // so we cannot declare this interface as a subtype. // see https://github.com/angular/angular/issues/3379#issuecomment-126169417 (target: Object, propertyKey?: string|symbol, parameterIndex?: number): void; - - /** - * Storage for the accumulated annotations so far used by the DSL syntax. - * - * Used by {@link Class} to annotate the generated class. - */ - annotations: any[]; - - /** - * Generate a class from the definition and annotate it with {@link TypeDecorator#annotations}. - */ - Class(obj: ClassDefinition): Type; } -function extractAnnotation(annotation: any): any { - if (typeof annotation === 'function' && annotation.hasOwnProperty('annotation')) { - // it is a decorator, extract annotation - annotation = annotation.annotation; - } - return annotation; -} - -function applyParams(fnOrArray: Function | any[] | undefined, key: string): Function { - if (fnOrArray === Object || fnOrArray === String || fnOrArray === Function || - fnOrArray === Number || fnOrArray === Array) { - throw new Error(`Can not use native ${stringify(fnOrArray)} as constructor`); - } - - if (typeof fnOrArray === 'function') { - return fnOrArray; - } - - if (Array.isArray(fnOrArray)) { - const annotations: any[] = fnOrArray as any[]; - const annoLength = annotations.length - 1; - const fn: Function = fnOrArray[annoLength]; - if (typeof fn !== 'function') { - throw new Error( - `Last position of Class method array must be Function in key ${key} was '${stringify(fn)}'`); - } - if (annoLength != fn.length) { - throw new Error( - `Number of annotations (${annoLength}) does not match number of arguments (${fn.length}) in the function: ${stringify(fn)}`); - } - const paramsAnnotations: any[][] = []; - for (let i = 0, ii = annotations.length - 1; i < ii; i++) { - const paramAnnotations: any[] = []; - paramsAnnotations.push(paramAnnotations); - const annotation = annotations[i]; - if (Array.isArray(annotation)) { - for (let j = 0; j < annotation.length; j++) { - paramAnnotations.push(extractAnnotation(annotation[j])); - } - } else if (typeof annotation === 'function') { - paramAnnotations.push(extractAnnotation(annotation)); - } else { - paramAnnotations.push(annotation); - } - } - Reflect.defineMetadata('parameters', paramsAnnotations, fn); - return fn; - } - - throw new Error( - `Only Function or Array is supported in Class definition for key '${key}' is '${stringify(fnOrArray)}'`); -} - -/** - * Provides a way for expressing ES6 classes with parameter annotations in ES5. - * - * ## Basic Example - * - * ``` - * var Greeter = ng.Class({ - * constructor: function(name) { - * this.name = name; - * }, - * - * greet: function() { - * alert('Hello ' + this.name + '!'); - * } - * }); - * ``` - * - * is equivalent to ES6: - * - * ``` - * class Greeter { - * constructor(name) { - * this.name = name; - * } - * - * greet() { - * alert('Hello ' + this.name + '!'); - * } - * } - * ``` - * - * or equivalent to ES5: - * - * ``` - * var Greeter = function (name) { - * this.name = name; - * } - * - * Greeter.prototype.greet = function () { - * alert('Hello ' + this.name + '!'); - * } - * ``` - * - * ### Example with parameter annotations - * - * ``` - * var MyService = ng.Class({ - * constructor: [String, [new Optional(), Service], function(name, myService) { - * ... - * }] - * }); - * ``` - * - * is equivalent to ES6: - * - * ``` - * class MyService { - * constructor(name: string, @Optional() myService: Service) { - * ... - * } - * } - * ``` - * - * ### Example with inheritance - * - * ``` - * var Shape = ng.Class({ - * constructor: (color) { - * this.color = color; - * } - * }); - * - * var Square = ng.Class({ - * extends: Shape, - * constructor: function(color, size) { - * Shape.call(this, color); - * this.size = size; - * } - * }); - * ``` - * @suppress {globalThis} - * @stable - */ -export function Class(clsDef: ClassDefinition): Type { - const constructor = applyParams( - clsDef.hasOwnProperty('constructor') ? clsDef.constructor : undefined, 'constructor'); - - let proto = constructor.prototype; - - if (clsDef.hasOwnProperty('extends')) { - if (typeof clsDef.extends === 'function') { - (constructor).prototype = proto = - Object.create((clsDef.extends).prototype); - } else { - throw new Error( - `Class definition 'extends' property must be a constructor function was: ${stringify(clsDef.extends)}`); - } - } - - for (const key in clsDef) { - if (key !== 'extends' && key !== 'prototype' && clsDef.hasOwnProperty(key)) { - proto[key] = applyParams(clsDef[key], key); - } - } - - if (this && this.annotations instanceof Array) { - Reflect.defineMetadata('annotations', this.annotations, constructor); - } - - const constructorName = constructor['name']; - if (!constructorName || constructorName === 'constructor') { - (constructor as any)['overriddenName'] = `class${_nextClassId++}`; - } - - return >constructor; -} +export const ANNOTATIONS = '__annotations__'; +export const PARAMETERS = '__paramaters__'; +export const PROP_METADATA = '__prop__metadata__'; /** * @suppress {globalThis} @@ -268,27 +48,21 @@ export function makeDecorator( const metaCtor = makeMetadataCtor(props); function DecoratorFactory(objOrType: any): (cls: any) => any { - if (!(Reflect && Reflect.getOwnMetadata)) { - throw 'reflect-metadata shim is required when using class decorators'; - } - if (this instanceof DecoratorFactory) { metaCtor.call(this, objOrType); return this; } const annotationInstance = new (DecoratorFactory)(objOrType); - const chainAnnotation = - typeof this === 'function' && Array.isArray(this.annotations) ? this.annotations : []; - chainAnnotation.push(annotationInstance); const TypeDecorator: TypeDecorator = function TypeDecorator(cls: Type) { - const annotations = Reflect.getOwnMetadata('annotations', cls) || []; + // Use of Object.defineProperty is important since it creates non-enumerable property which + // prevents the property is copied during subclassing. + const annotations = cls.hasOwnProperty(ANNOTATIONS) ? + (cls as any)[ANNOTATIONS] : + Object.defineProperty(cls, ANNOTATIONS, {value: []})[ANNOTATIONS]; annotations.push(annotationInstance); - Reflect.defineMetadata('annotations', annotations, cls); return cls; }; - TypeDecorator.annotations = chainAnnotation; - TypeDecorator.Class = Class; if (chainFn) chainFn(TypeDecorator); return TypeDecorator; } @@ -327,7 +101,11 @@ export function makeParamDecorator( return ParamDecorator; function ParamDecorator(cls: any, unusedKey: any, index: number): any { - const parameters: (any[] | null)[] = Reflect.getOwnMetadata('parameters', cls) || []; + // Use of Object.defineProperty is important since it creates non-enumerable property which + // prevents the property is copied during subclassing. + const parameters = cls.hasOwnProperty(PARAMETERS) ? + (cls as any)[PARAMETERS] : + Object.defineProperty(cls, PARAMETERS, {value: []})[PARAMETERS]; // there might be gaps if some in between parameters do not have annotations. // we pad with nulls. @@ -335,10 +113,7 @@ export function makeParamDecorator( parameters.push(null); } - parameters[index] = parameters[index] || []; - parameters[index] !.push(annotationInstance); - - Reflect.defineMetadata('parameters', parameters, cls); + (parameters[index] = parameters[index] || []).push(annotationInstance); return cls; } } @@ -363,10 +138,14 @@ export function makePropDecorator( const decoratorInstance = new (PropDecoratorFactory)(...args); return function PropDecorator(target: any, name: string) { - const meta = Reflect.getOwnMetadata('propMetadata', target.constructor) || {}; + const constructor = target.constructor; + // Use of Object.defineProperty is important since it creates non-enumerable property which + // prevents the property is copied during subclassing. + const meta = constructor.hasOwnProperty(PROP_METADATA) ? + (constructor as any)[PROP_METADATA] : + Object.defineProperty(constructor, PROP_METADATA, {value: {}})[PROP_METADATA]; meta[name] = meta.hasOwnProperty(name) && meta[name] || []; meta[name].unshift(decoratorInstance); - Reflect.defineMetadata('propMetadata', meta, target.constructor); }; } diff --git a/packages/core/test/metadata/decorators_spec.ts b/packages/core/test/metadata/decorators_spec.ts deleted file mode 100644 index bff20fbf66..0000000000 --- a/packages/core/test/metadata/decorators_spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {Component, Directive} from '@angular/core'; -import {reflector} from '@angular/core/src/reflection/reflection'; -import {describe, expect, it} from '@angular/core/testing/src/testing_internal'; - -export function main() { - describe('es5 decorators', () => { - it('should declare directive class', () => { - const MyDirective = Directive({}).Class({constructor: function() { this.works = true; }}); - expect(new MyDirective().works).toEqual(true); - }); - - it('should declare Component class', () => { - const MyComponent = Component({}).Class({constructor: function() { this.works = true; }}); - expect(new MyComponent().works).toEqual(true); - }); - - it('should create type in ES5', () => { - class MyComponent {}; - let as: any /** TODO #9100 */; - (MyComponent).annotations = as = Component({}); - expect(reflector.annotations(MyComponent)).toEqual(as.annotations); - }); - }); -} diff --git a/packages/core/test/util/decorators_spec.ts b/packages/core/test/util/decorators_spec.ts index aa7a8cadda..7de8af451f 100644 --- a/packages/core/test/util/decorators_spec.ts +++ b/packages/core/test/util/decorators_spec.ts @@ -6,17 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {Inject} from '@angular/core'; -import {reflector} from '@angular/core/src/reflection/reflection'; -import {global} from '@angular/core/src/util'; -import {Class, makeDecorator, makePropDecorator} from '@angular/core/src/util/decorators'; +import {reflector} from '../../src/reflection/reflection'; +import {ANNOTATIONS, makeDecorator, makePropDecorator} from '../../src/util/decorators'; class DecoratedParent {} class DecoratedChild extends DecoratedParent {} export function main() { - const Reflect = global['Reflect']; - const TerminalDecorator = makeDecorator('TerminalDecorator', (data: any) => ({terminal: true, ...data})); const TestDecorator = makeDecorator( @@ -54,7 +50,7 @@ export function main() { it('should invoke as decorator', () => { function Type() {} TestDecorator({marker: 'WORKS'})(Type); - const annotations = Reflect.getOwnMetadata('annotations', Type); + const annotations = (Type as any)[ANNOTATIONS]; expect(annotations[0].marker).toEqual('WORKS'); }); @@ -64,102 +60,13 @@ export function main() { expect(annotation.marker).toEqual('WORKS'); }); - it('should invoke as chain', () => { - let chain: any = TestDecorator({marker: 'WORKS'}); - expect(typeof chain.Terminal).toEqual('function'); - chain = chain.Terminal(); - expect(chain.annotations[0] instanceof TestDecorator).toEqual(true); - expect(chain.annotations[0].marker).toEqual('WORKS'); - expect(chain.annotations[1] instanceof TerminalDecorator).toEqual(true); - }); - it('should not apply decorators from the prototype chain', function() { TestDecorator({marker: 'parent'})(DecoratedParent); TestDecorator({marker: 'child'})(DecoratedChild); - const annotations = Reflect.getOwnMetadata('annotations', DecoratedChild); + const annotations = (DecoratedChild as any)[ANNOTATIONS]; expect(annotations.length).toBe(1); expect(annotations[0].marker).toEqual('child'); }); - - describe('Class', () => { - it('should create a class', () => { - let i0: any; - let i1: any; - const MyClass = (TestDecorator({marker: 'test-works'})).Class({ - extends: Class({ - constructor: function() {}, - extendWorks: function() { return 'extend ' + this.arg; } - }), - constructor: [String, function(arg: any) { this.arg = arg; }], - methodA: [ - i0 = new Inject(String), - [i1 = Inject(String), Number], - function(a: any, b: any) {}, - ], - works: function() { return this.arg; }, - prototype: 'IGNORE' - }); - - const obj: any = new MyClass('WORKS'); - expect(obj.arg).toEqual('WORKS'); - expect(obj.works()).toEqual('WORKS'); - expect(obj.extendWorks()).toEqual('extend WORKS'); - expect(reflector.parameters(MyClass)).toEqual([[String]]); - expect(reflector.parameters(obj.methodA)).toEqual([[i0], [i1.annotation, Number]]); - - const proto = (MyClass).prototype; - expect(proto.extends).toEqual(undefined); - expect(proto.prototype).toEqual(undefined); - - expect(reflector.annotations(MyClass)[0].marker).toEqual('test-works'); - }); - - describe('errors', () => { - it('should ensure that last constructor is required', () => { - expect(() => { (Class)({}); }) - .toThrowError( - 'Only Function or Array is supported in Class definition for key \'constructor\' is \'undefined\''); - }); - - - it('should ensure that we dont accidentally patch native objects', () => { - expect(() => { - (Class)({constructor: Object}); - }).toThrowError('Can not use native Object as constructor'); - }); - - - it('should ensure that last position is function', () => { - expect(() => { Class({constructor: []}); }) - .toThrowError( - 'Last position of Class method array must be Function in key constructor was \'undefined\''); - }); - - it('should ensure that annotation count matches parameters count', () => { - expect(() => { - Class({constructor: [String, function MyType() {}]}); - }) - .toThrowError( - 'Number of annotations (1) does not match number of arguments (0) in the function: MyType'); - }); - - it('should ensure that only Function|Arrays are supported', () => { - expect(() => { Class({constructor: function() {}, method: 'non_function'}); }) - .toThrowError( - 'Only Function or Array is supported in Class definition for key \'method\' is \'non_function\''); - }); - - it('should ensure that extends is a Function', () => { - expect(() => { Class({extends: 'non_type', constructor: function() {}}); }) - .toThrowError( - 'Class definition \'extends\' property must be a constructor function was: non_type'); - }); - - it('should assign an overridden name for anonymous constructor functions', () => { - expect((Class({constructor: function() {}}) as any).overriddenName).not.toBeUndefined(); - }); - }); - }); }); } diff --git a/packages/upgrade/src/dynamic/upgrade_adapter.ts b/packages/upgrade/src/dynamic/upgrade_adapter.ts index f4e744007b..8cc8e65c36 100644 --- a/packages/upgrade/src/dynamic/upgrade_adapter.ts +++ b/packages/upgrade/src/dynamic/upgrade_adapter.ts @@ -551,19 +551,19 @@ export class UpgradeAdapter { .then(() => { // At this point we have ng1 injector and we have prepared // ng1 components to be upgraded, we now can bootstrap ng2. - const DynamicNgUpgradeModule = - NgModule({ - providers: [ - {provide: $INJECTOR, useFactory: () => ng1Injector}, - {provide: $COMPILE, useFactory: () => ng1Injector.get($COMPILE)}, - this.upgradedProviders - ], - imports: [this.ng2AppModule], - entryComponents: this.downgradedComponents - }).Class({ - constructor: function DynamicNgUpgradeModule() {}, - ngDoBootstrap: function() {} - }); + @NgModule({ + providers: [ + {provide: $INJECTOR, useFactory: () => ng1Injector}, + {provide: $COMPILE, useFactory: () => ng1Injector.get($COMPILE)}, + this.upgradedProviders + ], + imports: [this.ng2AppModule], + entryComponents: this.downgradedComponents + }) + class DynamicNgUpgradeModule { + constructor() {} + ngDoBootstrap() {} + } (platformRef as any) ._bootstrapModuleWithZone( DynamicNgUpgradeModule, this.compilerOptions, this.ngZone) diff --git a/packages/upgrade/src/dynamic/upgrade_ng1_adapter.ts b/packages/upgrade/src/dynamic/upgrade_ng1_adapter.ts index 366fd82f0b..59d5bace78 100644 --- a/packages/upgrade/src/dynamic/upgrade_ng1_adapter.ts +++ b/packages/upgrade/src/dynamic/upgrade_ng1_adapter.ts @@ -38,23 +38,26 @@ export class UpgradeNg1ComponentAdapterBuilder { name.replace(CAMEL_CASE, (all: string, next: string) => '-' + next.toLowerCase()); const self = this; - this.type = - Directive({selector: selector, inputs: this.inputsRename, outputs: this.outputsRename}) - .Class({ - constructor: [ - new Inject($SCOPE), Injector, ElementRef, - function(scope: angular.IScope, injector: Injector, elementRef: ElementRef) { - const helper = new UpgradeHelper(injector, name, elementRef, this.directive); - return new UpgradeNg1ComponentAdapter( - helper, scope, self.template, self.inputs, self.outputs, self.propertyOutputs, - self.checkProperties, self.propertyMap); - } - ], - ngOnInit: function() { /* needs to be here for ng2 to properly detect it */ }, - ngOnChanges: function() { /* needs to be here for ng2 to properly detect it */ }, - ngDoCheck: function() { /* needs to be here for ng2 to properly detect it */ }, - ngOnDestroy: function() { /* needs to be here for ng2 to properly detect it */ }, - }); + @Directive({selector: selector, inputs: this.inputsRename, outputs: this.outputsRename}) + class MyClass { + directive: angular.IDirective; + constructor( + @Inject($SCOPE) scope: angular.IScope, injector: Injector, elementRef: ElementRef) { + const helper = new UpgradeHelper(injector, name, elementRef, this.directive); + return new UpgradeNg1ComponentAdapter( + helper, scope, self.template, self.inputs, self.outputs, self.propertyOutputs, + self.checkProperties, self.propertyMap) as any; + } + ngOnInit() { /* needs to be here for ng2 to properly detect it */ + } + ngOnChanges() { /* needs to be here for ng2 to properly detect it */ + } + ngDoCheck() { /* needs to be here for ng2 to properly detect it */ + } + ngOnDestroy() { /* needs to be here for ng2 to properly detect it */ + } + }; + this.type = MyClass; } extractBindings() { diff --git a/packages/upgrade/test/dynamic/upgrade_spec.ts b/packages/upgrade/test/dynamic/upgrade_spec.ts index ff35abb500..9a32de8acd 100644 --- a/packages/upgrade/test/dynamic/upgrade_spec.ts +++ b/packages/upgrade/test/dynamic/upgrade_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ChangeDetectorRef, Class, Component, EventEmitter, Input, NO_ERRORS_SCHEMA, NgModule, NgZone, OnChanges, SimpleChange, SimpleChanges, Testability, destroyPlatform, forwardRef} from '@angular/core'; +import {ChangeDetectorRef, Component, EventEmitter, Input, NO_ERRORS_SCHEMA, NgModule, NgZone, OnChanges, SimpleChange, SimpleChanges, Testability, destroyPlatform, forwardRef} from '@angular/core'; import {async, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; @@ -25,14 +25,16 @@ export function main() { it('should instantiate ng2 in ng1 template and project content', async(() => { const ng1Module = angular.module('ng1', []); - const Ng2 = Component({ - selector: 'ng2', - template: `{{ 'NG2' }}()`, - }).Class({constructor: function() {}}); + @Component({ + selector: 'ng2', + template: `{{ 'NG2' }}()`, + }) + class Ng2 { + } - const Ng2Module = NgModule({declarations: [Ng2], imports: [BrowserModule]}).Class({ - constructor: function() {} - }); + @NgModule({declarations: [Ng2], imports: [BrowserModule]}) + class Ng2Module { + } const element = html('
{{ \'ng1[\' }}~{{ \'ng-content\' }}~{{ \']\' }}
'); @@ -49,15 +51,19 @@ export function main() { const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const ng1Module = angular.module('ng1', []); - const Ng2 = Component({ - selector: 'ng2', - template: `{{ 'ng2(' }}{{'transclude'}}{{ ')' }}`, - }).Class({constructor: function Ng2() {}}); + @Component({ + selector: 'ng2', + template: `{{ 'ng2(' }}{{'transclude'}}{{ ')' }}`, + }) + class Ng2 { + }; - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2], - imports: [BrowserModule], - }).Class({constructor: function Ng2Module() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng1', () => { return {transclude: true, template: '{{ "ng1" }}()'}; @@ -77,19 +83,20 @@ export function main() { spyOn(platformRef, '_bootstrapModuleWithZone').and.callThrough(); const ng1Module = angular.module('ng1', []); - const Ng2 = Component({ - selector: 'ng2', - template: `{{ 'NG2' }}()` - }).Class({constructor: function() {}}); + @Component({selector: 'ng2', template: `{{ 'NG2' }}()`}) + class Ng2 { + } const element = html('
{{ \'ng1[\' }}~{{ \'ng-content\' }}~{{ \']\' }}
'); - const Ng2AppModule = - NgModule({ - declarations: [Ng2], - imports: [BrowserModule], - }).Class({constructor: function Ng2AppModule() {}, ngDoBootstrap: function() {}}); + @NgModule({ + declarations: [Ng2], + imports: [BrowserModule], + }) + class Ng2AppModule { + ngDoBootstrap() {} + }; const adapter: UpgradeAdapter = new UpgradeAdapter(Ng2AppModule, {providers: []}); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); @@ -107,15 +114,19 @@ export function main() { beforeEach(() => { angular.module('ng1', []); - const ng2Component = Component({ - selector: 'ng2', - template: ``, - }).Class({constructor: function() {}}); + @Component({ + selector: 'ng2', + template: ``, + }) + class ng2Component { + } - const Ng2Module = NgModule({ - declarations: [ng2Component], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [ng2Component], + imports: [BrowserModule], + }) + class Ng2Module { + } adapter = new UpgradeAdapter(Ng2Module); }); @@ -161,18 +172,22 @@ export function main() { $rootScope.reset = () => log.length = 0; }); - const Ng2 = Component({ - selector: 'ng2', - template: `{{l('2A')}}{{l('2B')}}{{l('2C')}}` - }).Class({constructor: function() { this.l = l; }}); + @Component({ + selector: 'ng2', + template: `{{l('2A')}}{{l('2B')}}{{l('2C')}}` + }) + class Ng2 { + l: any; + constructor() { this.l = l; } + } - const Ng2Module = - NgModule({ - declarations: [ - adapter.upgradeNg1Component('ng1a'), adapter.upgradeNg1Component('ng1b'), Ng2 - ], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: + [adapter.upgradeNg1Component('ng1a'), adapter.upgradeNg1Component('ng1b'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); @@ -302,37 +317,35 @@ export function main() { $rootScope.eventA = '?'; $rootScope.eventB = '?'; }); - const Ng2 = Component({ - selector: 'ng2', - inputs: - ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], - outputs: [ - 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', - 'twoWayBEmitter: twoWayBChange' - ], - template: 'ignore: {{ignore}}; ' + - 'literal: {{literal}}; interpolate: {{interpolate}}; ' + - 'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' + - 'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})' - }).Class({ - constructor: function() { - this.ngOnChangesCount = 0; - this.ignore = '-'; - this.literal = '?'; - this.interpolate = '?'; - this.oneWayA = '?'; - this.oneWayB = '?'; - this.twoWayA = '?'; - this.twoWayB = '?'; - this.eventA = new EventEmitter(); - this.eventB = new EventEmitter(); - this.twoWayAEmitter = new EventEmitter(); - this.twoWayBEmitter = new EventEmitter(); - }, - ngOnChanges: function(changes: SimpleChanges) { + @Component({ + selector: 'ng2', + inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], + outputs: [ + 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'twoWayBEmitter: twoWayBChange' + ], + template: 'ignore: {{ignore}}; ' + + 'literal: {{literal}}; interpolate: {{interpolate}}; ' + + 'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' + + 'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})' + }) + class Ng2 { + ngOnChangesCount = 0; + ignore = '-'; + literal = '?'; + interpolate = '?'; + oneWayA = '?'; + oneWayB = '?'; + twoWayA = '?'; + twoWayB = '?'; + eventA = new EventEmitter(); + eventB = new EventEmitter(); + twoWayAEmitter = new EventEmitter(); + twoWayBEmitter = new EventEmitter(); + ngOnChanges(changes: SimpleChanges) { const assert = (prop: string, value: any) => { - if (this[prop] != value) { - throw new Error(`Expected: '${prop}' to be '${value}' but was '${this[prop]}'`); + if ((this as any)[prop] != value) { + throw new Error( + `Expected: '${prop}' to be '${value}' but was '${(this as any)[prop]}'`); } }; @@ -374,13 +387,15 @@ export function main() { throw new Error('Called too many times! ' + JSON.stringify(changes)); } } - }); + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); - const Ng2Module = NgModule({ - declarations: [Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + }; const element = html(`
| {{modelA}}
`); - const Ng2Module = NgModule({ - declarations: [Ng2], - imports: [BrowserModule], - schemas: [NO_ERRORS_SCHEMA], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [Ng2], + imports: [BrowserModule], + schemas: [NO_ERRORS_SCHEMA], + }) + class Ng2Module { + } adapter.bootstrap(element, ['ng1']).ready((ref) => { let $rootScope: any = ref.ng1RootScope; @@ -537,15 +554,17 @@ export function main() { }; }); - const Ng2 = Component({selector: 'ng2', template: 'test'}).Class({ - constructor: function() {}, - ngOnDestroy: function() { onDestroyed.emit('destroyed'); } - }); + @Component({selector: 'ng2', template: 'test'}) + class Ng2 { + ngOnDestroy() { onDestroyed.emit('destroyed'); } + } - const Ng2Module = NgModule({ - declarations: [Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); const element = html(''); @@ -571,13 +590,16 @@ export function main() { } ]); - const Ng2 = - Component({selector: 'ng2', template: 'test'}).Class({constructor: function() {}}); + @Component({selector: 'ng2', template: 'test'}) + class Ng2 { + }; - const Ng2Module = NgModule({ - declarations: [Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); const element = html(''); @@ -590,15 +612,17 @@ export function main() { it('should support multi-slot projection', async(() => { const ng1Module = angular.module('ng1', []); - const Ng2 = Component({ - selector: 'ng2', - template: '2a()' + - '2b()' - }).Class({constructor: function() {}}); + @Component({ + selector: 'ng2', + template: '2a()' + + '2b()' + }) + class Ng2 { + } - const Ng2Module = NgModule({declarations: [Ng2], imports: [BrowserModule]}).Class({ - constructor: function() {} - }); + @NgModule({declarations: [Ng2], imports: [BrowserModule]}) + class Ng2Module { + } // The ng-if on one of the projected children is here to make sure // the correct slot is targeted even with structural directives in play. @@ -942,27 +966,27 @@ export function main() { }; }; ng1Module.directive('ng1', ng1); - const Ng2 = - Component({ - selector: 'ng2', - template: - '' + - '' + - '{{event}}-{{last}}, {{first}}, {{city}}' - }).Class({ - constructor: function() { - this.first = 'Victor'; - this.last = 'Savkin'; - this.city = 'SF'; - this.event = '?'; - } - }); + @Component({ + selector: 'ng2', + template: + '' + + '' + + '{{event}}-{{last}}, {{first}}, {{city}}' + }) + class Ng2 { + first = 'Victor'; + last = 'Savkin'; + city = 'SF'; + event = '?'; + } - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); const element = html(`
`); @@ -989,23 +1013,24 @@ export function main() { }; }; ng1Module.directive('ng1', ng1); - const Ng2 = Component({ - selector: 'ng2', - template: '' + - '' + - '' + - '' - }).Class({ - constructor: function() { - this.first = 'Victor'; - this.last = 'Savkin'; - } - }); + @Component({ + selector: 'ng2', + template: '' + + '' + + '' + + '' + }) + class Ng2 { + first = 'Victor'; + last = 'Savkin'; + } - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); const element = html(`
`); @@ -1036,23 +1061,22 @@ export function main() { }; ng1Module.directive('ng1', ng1); - const Ng2 = - Component({ - selector: 'ng2', - template: - '{{someText}} - Length: {{dataList.length}} | ' - }).Class({ + @Component({ + selector: 'ng2', + template: + '{{someText}} - Length: {{dataList.length}} | ' + }) + class Ng2 { + dataList = [1, 2, 3]; + someText = 'ng2'; + } - constructor: function() { - this.dataList = [1, 2, 3]; - this.someText = 'ng2'; - } - }); - - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); const element = html(`
`); @@ -1081,23 +1105,22 @@ export function main() { }; ng1Module.directive('ng1', ng1); - const Ng2 = - Component({ - selector: 'ng2', - template: - '{{someText}} - Length: {{dataList.length}} | ' - }).Class({ + @Component({ + selector: 'ng2', + template: + '{{someText}} - Length: {{dataList.length}} | ' + }) + class Ng2 { + dataList = [1, 2, 3]; + someText = 'ng2'; + } - constructor: function() { - this.dataList = [1, 2, 3]; - this.someText = 'ng2'; - } - }); - - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); const element = html(`
`); @@ -1122,14 +1145,16 @@ export function main() { const ng1 = () => { return {templateUrl: 'url.html'}; }; ng1Module.directive('ng1', ng1); - const Ng2 = Component({selector: 'ng2', template: ''}).Class({ - constructor: function() {} - }); + @Component({selector: 'ng2', template: ''}) + class Ng2 { + } - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); const element = html(`
`); @@ -1149,14 +1174,16 @@ export function main() { const ng1 = () => { return {templateUrl() { return 'url.html'; }}; }; ng1Module.directive('ng1', ng1); - const Ng2 = Component({selector: 'ng2', template: ''}).Class({ - constructor: function() {} - }); + @Component({selector: 'ng2', template: ''}) + class Ng2 { + } - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); const element = html(`
`); @@ -1173,14 +1200,16 @@ export function main() { const ng1 = () => { return {template: ''}; }; ng1Module.directive('ng1', ng1); - const Ng2 = Component({selector: 'ng2', template: ''}).Class({ - constructor: function() {} - }); + @Component({selector: 'ng2', template: ''}) + class Ng2 { + } - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); const element = html(`
`); @@ -1197,14 +1226,16 @@ export function main() { const ng1 = () => { return {template() { return ''; }}; }; ng1Module.directive('ng1', ng1); - const Ng2 = Component({selector: 'ng2', template: ''}).Class({ - constructor: function() {} - }); + @Component({selector: 'ng2', template: ''}) + class Ng2 { + } - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); const element = html(`
`); @@ -1222,14 +1253,16 @@ export function main() { const ng1 = () => { return {templateUrl: 'url.html'}; }; ng1Module.directive('ng1', ng1); - const Ng2 = Component({selector: 'ng2', template: ''}).Class({ - constructor: function() {} - }); + @Component({selector: 'ng2', template: ''}) + class Ng2 { + } - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); const element = html(`
`); @@ -1249,30 +1282,31 @@ export function main() { template: '{{ctl.scope}}; {{ctl.isClass}}; {{ctl.hasElement}}; {{ctl.isPublished()}}', controllerAs: 'ctl', - controller: Class({ - constructor: function($scope: any, $element: any) { - (this).verifyIAmAClass(); + controller: class { + scope: any; hasElement: string; $element: any; isClass: any; + constructor($scope: any, $element: any) { + this.verifyIAmAClass(); this.scope = $scope.$parent.$parent == $scope.$root ? 'scope' : 'wrong-scope'; this.hasElement = $element[0].nodeName; this.$element = $element; - }, - verifyIAmAClass: function() { this.isClass = 'isClass'; }, - isPublished: function() { + } verifyIAmAClass() { this.isClass = 'isClass'; } isPublished() { return this.$element.controller('ng1') == this ? 'published' : 'not-published'; } - }) + } }; }; ng1Module.directive('ng1', ng1); - const Ng2 = Component({selector: 'ng2', template: ''}).Class({ - constructor: function() {} - }); + @Component({selector: 'ng2', template: ''}) + class Ng2 { + } - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); const element = html(`
`); @@ -1292,19 +1326,21 @@ export function main() { bindToController: true, template: '{{ctl.title}}', controllerAs: 'ctl', - controller: Class({constructor: function() {}}) + controller: class {} }; }; ng1Module.directive('ng1', ng1); - const Ng2 = Component({selector: 'ng2', template: ''}).Class({ - constructor: function() {} - }); + @Component({selector: 'ng2', template: ''}) + class Ng2 { + } - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); const element = html(`
`); @@ -1324,19 +1360,21 @@ export function main() { bindToController: {title: '@'}, template: '{{ctl.title}}', controllerAs: 'ctl', - controller: Class({constructor: function() {}}) + controller: class {} }; }; ng1Module.directive('ng1', ng1); - const Ng2 = Component({selector: 'ng2', template: ''}).Class({ - constructor: function() {} - }); + @Component({selector: 'ng2', template: ''}) + class Ng2 { + } - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); const element = html(`
`); @@ -1357,7 +1395,7 @@ export function main() { template: '{{ctl.status}}', require: 'ng1', controllerAs: 'ctrl', - controller: Class({constructor: function() { this.status = 'WORKS'; }}), + controller: class {status = 'WORKS';}, link: function(scope: any, element: any, attrs: any, linkController: any) { expect(scope.$root).toEqual($rootScope); expect(element[0].nodeName).toEqual('NG1'); @@ -1368,14 +1406,16 @@ export function main() { }; ng1Module.directive('ng1', ng1); - const Ng2 = Component({selector: 'ng2', template: ''}).Class({ - constructor: function() {} - }); + @Component({selector: 'ng2', template: ''}) + class Ng2 { + } - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); const element = html(`
`); @@ -1389,9 +1429,7 @@ export function main() { const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const ng1Module = angular.module('ng1', []); - const parent = () => { - return {controller: Class({constructor: function() { this.parent = 'PARENT'; }})}; - }; + const parent = () => { return {controller: class {parent = 'PARENT';}}; }; const ng1 = () => { return { scope: {title: '@'}, @@ -1399,7 +1437,7 @@ export function main() { template: '{{parent.parent}}:{{ng1.status}}', require: ['ng1', '^parent', '?^^notFound'], controllerAs: 'ctrl', - controller: Class({constructor: function() { this.status = 'WORKS'; }}), + controller: class {status = 'WORKS';}, link: function(scope: any, element: any, attrs: any, linkControllers: any) { expect(linkControllers[0].status).toEqual('WORKS'); expect(linkControllers[1].parent).toEqual('PARENT'); @@ -1412,14 +1450,16 @@ export function main() { ng1Module.directive('parent', parent); ng1Module.directive('ng1', ng1); - const Ng2 = Component({selector: 'ng2', template: ''}).Class({ - constructor: function() {} - }); + @Component({selector: 'ng2', template: ''}) + class Ng2 { + } - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); const element = html(`
`); @@ -1835,7 +1875,8 @@ export function main() { } // On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3), - // `$animate` will use `setTimeout(..., 16.6)` instead. This timeout will still be on + // `$animate` will use `setTimeout(..., 16.6)` instead. This timeout will still be + // on // the queue at the end of the test, causing it to fail. // Mocking animations (via `ngAnimateMock`) avoids the issue. angular.module('ng1', ['ngAnimateMock']) @@ -1923,7 +1964,8 @@ export function main() { } // On browsers that don't support `requestAnimationFrame` (IE 9, Android <= 4.3), - // `$animate` will use `setTimeout(..., 16.6)` instead. This timeout will still be on + // `$animate` will use `setTimeout(..., 16.6)` instead. This timeout will still be + // on // the queue at the end of the test, causing it to fail. // Mocking animations (via `ngAnimateMock`) avoids the issue. angular.module('ng1', ['ngAnimateMock']) @@ -2618,19 +2660,21 @@ export function main() { const ng1 = { bindings: {personProfile: '<'}, template: 'Hello {{$ctrl.personProfile.firstName}} {{$ctrl.personProfile.lastName}}', - controller: Class({constructor: function() {}}) + controller: class {} }; ng1Module.component('ng1', ng1); - const Ng2 = - Component({selector: 'ng2', template: ''}).Class({ - constructor: function() { this.goku = {firstName: 'GOKU', lastName: 'SAN'}; } - }); + @Component({selector: 'ng2', template: ''}) + class Ng2 { + goku = {firstName: 'GOKU', lastName: 'SAN'}; + } - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); @@ -2650,19 +2694,22 @@ export function main() { }; ng1Module.component('ng1', ng1); - const Ng2a = Component({selector: 'ng2a', template: 'ng2a()'}).Class({ - constructor: function() {} - }); + @Component({selector: 'ng2a', template: 'ng2a()'}) + class Ng2a { + } ng1Module.directive('ng2a', adapter.downgradeNg2Component(Ng2a)); - const Ng2b = - Component({selector: 'ng2b', template: 'ng2b'}).Class({constructor: function() {}}); + @Component({selector: 'ng2b', template: 'ng2b'}) + class Ng2b { + } ng1Module.directive('ng2b', adapter.downgradeNg2Component(Ng2b)); - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2a, Ng2b], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2a, Ng2b], + imports: [BrowserModule], + }) + class Ng2Module { + } const element = html(`
`); adapter.bootstrap(element, ['ng1']).ready((ref) => { @@ -2675,10 +2722,12 @@ export function main() { function SomeToken() {} it('should export ng2 instance to ng1', async(() => { - const MyNg2Module = NgModule({ - providers: [{provide: SomeToken, useValue: 'correct_value'}], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + providers: [{provide: SomeToken, useValue: 'correct_value'}], + imports: [BrowserModule], + }) + class MyNg2Module { + } const adapter: UpgradeAdapter = new UpgradeAdapter(MyNg2Module); const module = angular.module('myExample', []); @@ -2690,8 +2739,9 @@ export function main() { })); it('should export ng1 instance to ng2', async(() => { - const MyNg2Module = - NgModule({imports: [BrowserModule]}).Class({constructor: function() {}}); + @NgModule({imports: [BrowserModule]}) + class MyNg2Module { + }; const adapter: UpgradeAdapter = new UpgradeAdapter(MyNg2Module); const module = angular.module('myExample', []); @@ -2710,18 +2760,17 @@ export function main() { it('should respect hierarchical dependency injection for ng2', async(() => { const ng1Module = angular.module('ng1', []); - const Ng2Parent = Component({ - selector: 'ng2-parent', - template: `ng2-parent()` - }).Class({constructor: function() {}}); - const Ng2Child = Component({selector: 'ng2-child', template: `ng2-child`}).Class({ - constructor: [Ng2Parent, function(parent: any) {}] - }); + @Component({selector: 'ng2-parent', template: `ng2-parent()`}) + class Ng2Parent { + } + @Component({selector: 'ng2-child', template: `ng2-child`}) + class Ng2Child { + constructor(parent: Ng2Parent) {} + } - const Ng2Module = - NgModule({declarations: [Ng2Parent, Ng2Child], imports: [BrowserModule]}).Class({ - constructor: function() {} - }); + @NgModule({declarations: [Ng2Parent, Ng2Child], imports: [BrowserModule]}) + class Ng2Module { + } const element = html(''); @@ -2737,8 +2786,9 @@ export function main() { describe('testability', () => { it('should handle deferred bootstrap', async(() => { - const MyNg2Module = - NgModule({imports: [BrowserModule]}).Class({constructor: function() {}}); + @NgModule({imports: [BrowserModule]}) + class MyNg2Module { + } const adapter: UpgradeAdapter = new UpgradeAdapter(MyNg2Module); angular.module('ng1', []); @@ -2759,8 +2809,9 @@ export function main() { })); it('should wait for ng2 testability', async(() => { - const MyNg2Module = - NgModule({imports: [BrowserModule]}).Class({constructor: function() {}}); + @NgModule({imports: [BrowserModule]}) + class MyNg2Module { + } const adapter: UpgradeAdapter = new UpgradeAdapter(MyNg2Module); angular.module('ng1', []); @@ -2797,17 +2848,20 @@ export function main() { }; module.directive('ng1', ng1); - const Ng2 = - Component({ - selector: 'ng2', - inputs: ['name'], - template: 'ng2[transclude]()' - }).Class({constructor: function() {}}); + @Component({ + selector: 'ng2', + inputs: ['name'], + template: 'ng2[transclude]()' + }) + class Ng2 { + } - const Ng2Module = NgModule({ - declarations: [adapter.upgradeNg1Component('ng1'), Ng2], - imports: [BrowserModule], - }).Class({constructor: function() {}}); + @NgModule({ + declarations: [adapter.upgradeNg1Component('ng1'), Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } module.directive('ng2', adapter.downgradeNg2Component(Ng2)); @@ -2829,14 +2883,16 @@ export function main() { beforeEach(() => { const ng1Module = angular.module('ng1', []); - const Ng2 = Component({ - selector: 'ng2', - template: 'Hello World', - }).Class({constructor: function() {}}); + @Component({ + selector: 'ng2', + template: 'Hello World', + }) + class Ng2 { + } - const Ng2Module = NgModule({declarations: [Ng2], imports: [BrowserModule]}).Class({ - constructor: function() {} - }); + @NgModule({declarations: [Ng2], imports: [BrowserModule]}) + class Ng2Module { + } const upgradeAdapter = new UpgradeAdapter(Ng2Module); ng1Module.directive('ng2', upgradeAdapter.downgradeNg2Component(Ng2)); diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 936b0a0a58..97233f5bc6 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -163,17 +163,6 @@ export declare abstract class ChangeDetectorRef { abstract reattach(): void; } -/** @stable */ -export declare function Class(clsDef: ClassDefinition): Type; - -/** @stable */ -export declare type ClassDefinition = { - extends?: Type; - constructor: Function | any[]; -} & { - [x: string]: Type | Function | any[]; -}; - /** @stable */ export interface ClassProvider { multi?: boolean; @@ -1029,10 +1018,8 @@ export declare const Type: FunctionConstructor; /** @stable */ export interface TypeDecorator { - annotations: any[]; (target: Object, propertyKey?: string | symbol, parameterIndex?: number): void; >(type: T): T; - Class(obj: ClassDefinition): Type; } /** @stable */ From 27d901a51d1440c2ffa50ee0fb9bfc80e01b1485 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Wed, 9 Aug 2017 13:45:45 -0700 Subject: [PATCH 18/83] refactor(compiler-cli): cleanup API for transformer based ngc This is in preparation for watch mode. --- packages/compiler-cli/index.ts | 2 +- .../src/diagnostics/check_types.ts | 9 +- packages/compiler-cli/src/main.ts | 114 ++++++--- packages/compiler-cli/src/ngc.ts | 68 ------ packages/compiler-cli/src/perform-compile.ts | 224 ++++++++--------- packages/compiler-cli/src/transformers/api.ts | 8 +- .../src/transformers/entry_points.ts | 5 +- .../compiler-cli/src/transformers/program.ts | 34 ++- packages/compiler-cli/test/main_spec.ts | 110 +++------ packages/compiler-cli/test/ngc_spec.ts | 230 ++++-------------- packages/compiler-cli/tsconfig-build.json | 1 - packages/tsc-wrapped/src/compiler_host.ts | 73 +++--- packages/tsc-wrapped/src/main.ts | 8 +- tools/ngc-wrapped/index.ts | 25 +- 14 files changed, 359 insertions(+), 552 deletions(-) delete mode 100644 packages/compiler-cli/src/ngc.ts diff --git a/packages/compiler-cli/index.ts b/packages/compiler-cli/index.ts index 482b8ecba8..e7095d0698 100644 --- a/packages/compiler-cli/index.ts +++ b/packages/compiler-cli/index.ts @@ -20,7 +20,7 @@ export {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Sp export * from './src/transformers/api'; export * from './src/transformers/entry_points'; -export {performCompilation} from './src/perform-compile'; +export {performCompilation, readConfiguration, formatDiagnostics, calcProjectFileAndBasePath, createNgCompilerOptions} 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/src/diagnostics/check_types.ts b/packages/compiler-cli/src/diagnostics/check_types.ts index 0029f2fe60..48584a8493 100644 --- a/packages/compiler-cli/src/diagnostics/check_types.ts +++ b/packages/compiler-cli/src/diagnostics/check_types.ts @@ -9,7 +9,7 @@ import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, GeneratedFile, NgAnalyzedModules, ParseSourceSpan, Statement, StaticReflector, TypeScriptEmitter, createAotCompiler} from '@angular/compiler'; import * as ts from 'typescript'; -import {Diagnostic, DiagnosticCategory} from '../transformers/api'; +import {Diagnostic} from '../transformers/api'; interface FactoryInfo { source: ts.SourceFile; @@ -143,7 +143,7 @@ export class TypeChecker { const diagnosticsList = diagnosticsFor(fileName); diagnosticsList.push({ message: diagnosticMessageToString(diagnostic.messageText), - category: diagnosticCategoryConverter(diagnostic.category), span + category: diagnostic.category, span }); } } @@ -166,11 +166,6 @@ function diagnosticMessageToString(message: ts.DiagnosticMessageChain | string): return ts.flattenDiagnosticMessageText(message, '\n'); } -function diagnosticCategoryConverter(kind: ts.DiagnosticCategory) { - // The diagnostics kind matches ts.DiagnosticCategory. Review this code if this changes. - return kind as any as DiagnosticCategory; -} - function createFactoryInfo(emitter: TypeScriptEmitter, file: GeneratedFile): FactoryInfo { const {sourceText, context} = emitter.emitStatementsAndContext(file.srcFileUrl, file.genFileUrl, file.stmts !); diff --git a/packages/compiler-cli/src/main.ts b/packages/compiler-cli/src/main.ts index 9e4f171760..28d9d86ab9 100644 --- a/packages/compiler-cli/src/main.ts +++ b/packages/compiler-cli/src/main.ts @@ -14,15 +14,87 @@ import * as ts from 'typescript'; import * as tsc from '@angular/tsc-wrapped'; import * as fs from 'fs'; import * as path from 'path'; -import * as ngc from './ngc'; +import * as api from './transformers/api'; +import * as ngc from './transformers/entry_points'; +import {performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration} from './perform-compile'; import {isSyntaxError} from '@angular/compiler'; - -import {readConfiguration} from './perform-compile'; - import {CodeGenerator} from './codegen'; -function codegen( +export function main( + args: string[], consoleError: (s: string) => void = console.error): Promise { + const parsedArgs = require('minimist')(args); + const {rootNames, options, errors: configErrors} = readCommandLineAndConfiguration(parsedArgs); + if (configErrors.length) { + return Promise.resolve(reportErrorsAndExit(options, configErrors, consoleError)); + } + if (options.disableTransformerPipeline) { + return disabledTransformerPipelineNgcMain(parsedArgs, consoleError); + } + const {diagnostics: compileDiags} = performCompilation(rootNames, options); + return Promise.resolve(reportErrorsAndExit(options, compileDiags, consoleError)); +} + +export function mainSync( + args: string[], consoleError: (s: string) => void = console.error): number { + const parsedArgs = require('minimist')(args); + const {rootNames, options, errors: configErrors} = readCommandLineAndConfiguration(parsedArgs); + if (configErrors.length) { + return reportErrorsAndExit(options, configErrors, consoleError); + } + const {diagnostics: compileDiags} = performCompilation(rootNames, options); + return reportErrorsAndExit(options, compileDiags, consoleError); +} + +function readCommandLineAndConfiguration(args: any): ParsedConfiguration { + const project = args.p || args.project || '.'; + const allDiagnostics: Diagnostics = []; + const config = readConfiguration(project); + const options = mergeCommandLineParams(args, config.options); + return {rootNames: config.rootNames, options, errors: config.errors}; +} + +function reportErrorsAndExit( + options: api.CompilerOptions, allDiagnostics: Diagnostics, + consoleError: (s: string) => void = console.error): number { + const exitCode = allDiagnostics.some(d => d.category === ts.DiagnosticCategory.Error) ? 1 : 0; + if (allDiagnostics.length) { + consoleError(formatDiagnostics(options, allDiagnostics)); + } + return exitCode; +} + +function mergeCommandLineParams( + cliArgs: {[k: string]: string}, options: api.CompilerOptions): api.CompilerOptions { + // TODO: also merge in tsc command line parameters by calling + // ts.readCommandLine. + if (cliArgs.i18nFile) options.i18nInFile = cliArgs.i18nFile; + if (cliArgs.i18nFormat) options.i18nInFormat = cliArgs.i18nFormat; + if (cliArgs.locale) options.i18nInLocale = cliArgs.locale; + const mt = cliArgs.missingTranslation; + if (mt === 'error' || mt === 'warning' || mt === 'ignore') { + options.i18nInMissingTranslations = mt; + } + return options; +} + +function disabledTransformerPipelineNgcMain( + args: any, consoleError: (s: string) => void = console.error): Promise { + const cliOptions = new tsc.NgcCliOptions(args); + const project = args.p || args.project || '.'; + return tsc.main(project, cliOptions, disabledTransformerPipelineCodegen) + .then(() => 0) + .catch(e => { + if (e instanceof tsc.UserError || isSyntaxError(e)) { + consoleError(e.message); + } else { + consoleError(e.stack); + } + return Promise.resolve(1); + }); +} + +function disabledTransformerPipelineCodegen( ngOptions: tsc.AngularCompilerOptions, cliOptions: tsc.NgcCliOptions, program: ts.Program, host: ts.CompilerHost) { if (ngOptions.enableSummariesForJit === undefined) { @@ -32,38 +104,8 @@ function codegen( return CodeGenerator.create(ngOptions, cliOptions, program, host).codegen(); } -export function main( - args: any, consoleError: (s: string) => void = console.error): Promise { - const project = args.p || args.project || '.'; - const cliOptions = new tsc.NgcCliOptions(args); - - return tsc.main(project, cliOptions, codegen).then(() => 0).catch(e => { - if (e instanceof tsc.UserError || isSyntaxError(e)) { - consoleError(e.message); - return Promise.resolve(1); - } else { - consoleError(e.stack); - consoleError('Compilation failed'); - return Promise.resolve(1); - } - }); -} - // CLI entry point if (require.main === module) { const args = process.argv.slice(2); - const parsedArgs = require('minimist')(args); - const project = parsedArgs.p || parsedArgs.project || '.'; - - const projectDir = fs.lstatSync(project).isFile() ? path.dirname(project) : project; - - // file names in tsconfig are resolved relative to this absolute path - const basePath = path.resolve(process.cwd(), projectDir); - const {ngOptions} = readConfiguration(project, basePath); - - if (ngOptions.disableTransformerPipeline) { - main(parsedArgs).then((exitCode: number) => process.exit(exitCode)); - } else { - process.exit(ngc.main(args, s => console.error(s))); - } + main(args).then((exitCode: number) => process.exitCode = exitCode); } diff --git a/packages/compiler-cli/src/ngc.ts b/packages/compiler-cli/src/ngc.ts deleted file mode 100644 index 788b1f4cb1..0000000000 --- a/packages/compiler-cli/src/ngc.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @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 - */ - -// Must be imported first, because Angular decorators throw on load. -import 'reflect-metadata'; - -import {isSyntaxError} from '@angular/compiler'; -import * as fs from 'fs'; -import * as path from 'path'; - -import {performCompilation, readConfiguration, throwOnDiagnostics} from './perform-compile'; -import {CompilerOptions} from './transformers/api'; - -export function main( - args: string[], consoleError: (s: string) => void = console.error, - checkFunc: (cwd: string, ...args: any[]) => void = throwOnDiagnostics): number { - try { - const parsedArgs = require('minimist')(args); - const project = parsedArgs.p || parsedArgs.project || '.'; - - const projectDir = fs.lstatSync(project).isFile() ? path.dirname(project) : project; - - // file names in tsconfig are resolved relative to this absolute path - const basePath = path.resolve(process.cwd(), projectDir); - const {parsed, ngOptions} = readConfiguration(project, basePath, checkFunc); - - // CLI arguments can override the i18n options - const ngcOptions = mergeCommandLine(parsedArgs, ngOptions); - - const res = performCompilation( - basePath, parsed.fileNames, parsed.options, ngcOptions, consoleError, checkFunc); - - return res.errorCode; - } catch (e) { - if (isSyntaxError(e)) { - consoleError(e.message); - return 1; - } - - consoleError(e.stack); - consoleError('Compilation failed'); - return 2; - } -} - -// Merge command line parameters -function mergeCommandLine( - parsedArgs: {[k: string]: string}, options: CompilerOptions): CompilerOptions { - if (parsedArgs.i18nFile) options.i18nInFile = parsedArgs.i18nFile; - if (parsedArgs.i18nFormat) options.i18nInFormat = parsedArgs.i18nFormat; - if (parsedArgs.locale) options.i18nInLocale = parsedArgs.locale; - const mt = parsedArgs.missingTranslation; - if (mt === 'error' || mt === 'warning' || mt === 'ignore') { - options.i18nInMissingTranslations = mt; - } - - return options; -} - -// CLI entry point -if (require.main === module) { - process.exit(main(process.argv.slice(2), s => console.error(s))); -} \ No newline at end of file diff --git a/packages/compiler-cli/src/perform-compile.ts b/packages/compiler-cli/src/perform-compile.ts index b7efcb2bf9..258a094ce8 100644 --- a/packages/compiler-cli/src/perform-compile.ts +++ b/packages/compiler-cli/src/perform-compile.ts @@ -17,24 +17,25 @@ import * as ng from './transformers/entry_points'; const TS_EXT = /\.ts$/; -export type Diagnostics = ts.Diagnostic[] | api.Diagnostic[]; +export type Diagnostics = Array; -function isTsDiagnostics(diagnostics: any): diagnostics is ts.Diagnostic[] { - return diagnostics && diagnostics[0] && (diagnostics[0].file || diagnostics[0].messageText); +function isTsDiagnostic(diagnostic: any): diagnostic is ts.Diagnostic { + return diagnostic && (diagnostic.file || diagnostic.messageText); } -function formatDiagnostics(cwd: string, diags: Diagnostics): string { +export function formatDiagnostics(options: api.CompilerOptions, 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]; + const tsFormatHost: ts.FormatDiagnosticsHost = { + getCurrentDirectory: () => options.basePath || process.cwd(), + getCanonicalFileName: fileName => fileName, + getNewLine: () => ts.sys.newLine + }; + return diags + .map(d => { + if (isTsDiagnostic(d)) { + return ts.formatDiagnostics([d], tsFormatHost); + } else { + let res = ts.DiagnosticCategory[d.category]; if (d.span) { res += ` at ${d.span.start.file.url}(${d.span.start.line + 1},${d.span.start.col + 1})`; @@ -45,134 +46,133 @@ function formatDiagnostics(cwd: string, diags: Diagnostics): string { res += `: ${d.message}\n`; } return res; - }) - .join(); - } + } + }) + .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('')); - } +export interface ParsedConfiguration { + options: api.CompilerOptions; + rootNames: string[]; + errors: Diagnostics; +} + +export function calcProjectFileAndBasePath(project: string): + {projectFile: string, basePath: string} { + const projectIsDir = fs.lstatSync(project).isDirectory(); + const projectFile = projectIsDir ? path.join(project, 'tsconfig.json') : project; + const projectDir = projectIsDir ? project : path.dirname(project); + const basePath = path.resolve(process.cwd(), projectDir); + return {projectFile, basePath}; +} + +export function createNgCompilerOptions( + basePath: string, config: any, tsOptions: ts.CompilerOptions): api.CompilerOptions { + return {...tsOptions, ...config.angularCompilerOptions, genDir: basePath, basePath}; } 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); + project: string, existingOptions?: ts.CompilerOptions): ParsedConfiguration { + try { + const {projectFile, basePath} = calcProjectFileAndBasePath(project); - 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); + let {config, error} = ts.readConfigFile(projectFile, ts.sys.readFile); - checkFunc(basePath, parsed.errors); + if (error) { + return {errors: [error], rootNames: [], options: {}}; + } + const parseConfigHost = { + useCaseSensitiveFileNames: true, + fileExists: fs.existsSync, + readDirectory: ts.sys.readDirectory, + readFile: ts.sys.readFile + }; + const parsed = + ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, existingOptions); + const rootNames = parsed.fileNames.map(f => path.normalize(f)); - // 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}; + const options = createNgCompilerOptions(basePath, config, parsed.options); + return {rootNames, options, errors: parsed.errors}; + } catch (e) { + const errors: Diagnostics = [{ + category: ts.DiagnosticCategory.Error, + message: e.stack, + }]; + return {errors, rootNames: [], options: {}}; + } } -/** - * Returns an object with two properties: - * - `errorCode` is 0 when the compilation was successful, - * - `result` is an `EmitResult` when the errorCode is 0, `undefined` otherwise. - */ export function performCompilation( - basePath: string, files: string[], options: ts.CompilerOptions, ngOptions: api.CompilerOptions, - consoleError: (s: string) => void = console.error, - checkFunc: (cwd: string, ...args: any[]) => void = throwOnDiagnostics, - tsCompilerHost?: ts.CompilerHost): {errorCode: number, result?: api.EmitResult} { + rootNames: string[], options: api.CompilerOptions, host?: api.CompilerHost, + oldProgram?: api.Program): { + program?: api.Program, + emitResult?: api.EmitResult, + diagnostics: Diagnostics, +} { const [major, minor] = ts.version.split('.'); - if (+major < 2 || (+major === 2 && +minor < 3)) { + if (Number(major) < 2 || (Number(major) === 2 && Number(minor) < 3)) { throw new Error('Must use TypeScript > 2.3 to have transformer support'); } + const allDiagnostics: Diagnostics = []; + + function checkDiagnostics(diags: Diagnostics | undefined) { + if (diags) { + allDiagnostics.push(...diags); + return diags.every(d => d.category !== ts.DiagnosticCategory.Error); + } + return true; + } + + let program: api.Program|undefined; + let emitResult: api.EmitResult|undefined; 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; + if (!host) { + host = ng.createNgCompilerHost({options}); } - const ngHostOptions = {...options, ...ngOptions}; - const ngHost = ng.createHost({tsHost: host, options: ngHostOptions}); - - const ngProgram = - ng.createProgram({rootNames: rootFileNames, host: ngHost, options: ngHostOptions}); + program = ng.createProgram({rootNames, host, options, oldProgram}); + let shouldEmit = true; // Check parameter diagnostics - checkFunc(basePath, ngProgram.getTsOptionDiagnostics(), ngProgram.getNgOptionDiagnostics()); + shouldEmit = shouldEmit && checkDiagnostics([ + ...program !.getTsOptionDiagnostics(), ...program !.getNgOptionDiagnostics() + ]); // Check syntactic diagnostics - checkFunc(basePath, ngProgram.getTsSyntacticDiagnostics()); + shouldEmit = shouldEmit && checkDiagnostics(program !.getTsSyntacticDiagnostics()); // Check TypeScript semantic and Angular structure diagnostics - checkFunc( - basePath, ngProgram.getTsSemanticDiagnostics(), ngProgram.getNgStructuralDiagnostics()); + shouldEmit = + shouldEmit && + checkDiagnostics( + [...program !.getTsSemanticDiagnostics(), ...program !.getNgStructuralDiagnostics()]); // Check Angular semantic diagnostics - checkFunc(basePath, ngProgram.getNgSemanticDiagnostics()); + shouldEmit = shouldEmit && checkDiagnostics(program !.getNgSemanticDiagnostics()); - const result = ngProgram.emit({ - emitFlags: api.EmitFlags.Default | - ((ngOptions.skipMetadataEmit || ngOptions.flatModuleOutFile) ? 0 : api.EmitFlags.Metadata) - }); - - checkFunc(basePath, result.diagnostics); - - return {errorCode: 0, result}; - } catch (e) { - if (isSyntaxError(e)) { - consoleError(e.message); - return {errorCode: 1}; + if (shouldEmit) { + const emitResult = program !.emit({ + emitFlags: api.EmitFlags.Default | + ((options.skipMetadataEmit || options.flatModuleOutFile) ? 0 : api.EmitFlags.Metadata) + }); + allDiagnostics.push(...emitResult.diagnostics); } - - throw e; + } catch (e) { + let errMsg: string; + if (isSyntaxError(e)) { + // don't report the stack for syntax errors as they are well known errors. + errMsg = e.message; + } else { + errMsg = e.stack; + } + allDiagnostics.push({ + category: ts.DiagnosticCategory.Error, + message: errMsg, + }); } + return {program, emitResult, diagnostics: allDiagnostics}; } \ No newline at end of file diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index c48c67327b..2c463c1437 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -9,16 +9,10 @@ import {ParseSourceSpan} from '@angular/compiler'; import * as ts from 'typescript'; -export enum DiagnosticCategory { - Warning = 0, - Error = 1, - Message = 2, -} - export interface Diagnostic { message: string; span?: ParseSourceSpan; - category: DiagnosticCategory; + category: ts.DiagnosticCategory; } export interface CompilerOptions extends ts.CompilerOptions { diff --git a/packages/compiler-cli/src/transformers/entry_points.ts b/packages/compiler-cli/src/transformers/entry_points.ts index 4e4f5b1721..b6d991a4c9 100644 --- a/packages/compiler-cli/src/transformers/entry_points.ts +++ b/packages/compiler-cli/src/transformers/entry_points.ts @@ -13,8 +13,9 @@ import {createModuleFilenameResolver} from './module_filename_resolver'; export {createProgram} from './program'; export {createModuleFilenameResolver}; -export function createHost({tsHost, options}: {tsHost: ts.CompilerHost, options: CompilerOptions}): - CompilerHost { +export function createNgCompilerHost( + {options, tsHost = ts.createCompilerHost(options, true)}: + {options: CompilerOptions, tsHost?: ts.CompilerHost}): CompilerHost { const resolver = createModuleFilenameResolver(tsHost, options); const host = Object.create(tsHost); diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index c1c0fe2da6..9e1cc3a84c 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -8,6 +8,7 @@ import {AotCompiler, AotCompilerOptions, GeneratedFile, NgAnalyzedModules, createAotCompiler, getParseErrors, isSyntaxError, toTypeScript} from '@angular/compiler'; import {MissingTranslationStrategy} from '@angular/core'; +import {createBundleIndexHost} from '@angular/tsc-wrapped'; import * as fs from 'fs'; import * as path from 'path'; import * as tsickle from 'tsickle'; @@ -16,7 +17,7 @@ import * as ts from 'typescript'; import {CompilerHost as AotCompilerHost} from '../compiler_host'; import {TypeChecker} from '../diagnostics/check_types'; -import {CompilerHost, CompilerOptions, Diagnostic, DiagnosticCategory, EmitFlags, EmitResult, Program} from './api'; +import {CompilerHost, CompilerOptions, Diagnostic, EmitFlags, EmitResult, Program} from './api'; import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions'; import {getAngularEmitterTransformFactory} from './node_emitter_transform'; @@ -31,8 +32,6 @@ const emptyModules: NgAnalyzedModules = { }; class AngularCompilerProgram implements Program { - // Initialized in the constructor - private oldTsProgram: ts.Program|undefined; private tsProgram: ts.Program; private aotCompilerHost: AotCompilerHost; private compiler: AotCompiler; @@ -49,12 +48,26 @@ class AngularCompilerProgram implements Program { private _generatedFileDiagnostics: Diagnostic[]|undefined; private _typeChecker: TypeChecker|undefined; private _semanticDiagnostics: Diagnostic[]|undefined; + private _optionsDiagnostics: Diagnostic[] = []; constructor( private rootNames: string[], private options: CompilerOptions, private host: CompilerHost, private oldProgram?: Program) { - this.oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined; - this.tsProgram = ts.createProgram(rootNames, options, host, this.oldTsProgram); + if (options.flatModuleOutFile && !options.skipMetadataEmit) { + const {host: bundleHost, indexName, errors} = createBundleIndexHost(options, rootNames, host); + if (errors) { + // TODO(tbosch): once we move MetadataBundler from tsc_wrapped into compiler_cli, + // directly create ng.Diagnostic instead of using ts.Diagnostic here. + this._optionsDiagnostics.push( + ...errors.map(e => ({category: e.category, message: e.messageText as string}))); + } else { + rootNames.push(indexName !); + this.host = host = bundleHost; + } + } + + const oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined; + this.tsProgram = ts.createProgram(rootNames, options, host, oldTsProgram); this.srcNames = this.tsProgram.getSourceFiles() .map(sf => sf.fileName) @@ -78,7 +91,7 @@ class AngularCompilerProgram implements Program { } getNgOptionDiagnostics(cancellationToken?: ts.CancellationToken): Diagnostic[] { - return getNgOptionDiagnostics(this.options); + return [...this._optionsDiagnostics, ...getNgOptionDiagnostics(this.options)]; } getTsSyntacticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): @@ -237,11 +250,11 @@ class AngularCompilerProgram implements Program { this._structuralDiagnostics = parserErrors.map(e => ({ message: e.contextualMessage(), - category: DiagnosticCategory.Error, + category: ts.DiagnosticCategory.Error, span: e.span })); } else { - this._structuralDiagnostics = [{message: e.message, category: DiagnosticCategory.Error}]; + this._structuralDiagnostics = [{message: e.message, category: ts.DiagnosticCategory.Error}]; } this._analyzedModules = emptyModules; return emptyModules; @@ -272,7 +285,8 @@ class AngularCompilerProgram implements Program { return this.options.skipTemplateCodegen ? [] : result; } catch (e) { if (isSyntaxError(e)) { - this._generatedFileDiagnostics = [{message: e.message, category: DiagnosticCategory.Error}]; + this._generatedFileDiagnostics = + [{message: e.message, category: ts.DiagnosticCategory.Error}]; return []; } throw e; @@ -395,7 +409,7 @@ function getNgOptionDiagnostics(options: CompilerOptions): Diagnostic[] { return [{ message: 'Angular compiler options "annotationsAs" only supports "static fields" and "decorators"', - category: DiagnosticCategory.Error + category: ts.DiagnosticCategory.Error }]; } } diff --git a/packages/compiler-cli/test/main_spec.ts b/packages/compiler-cli/test/main_spec.ts index a69e50c1b7..46b109846e 100644 --- a/packages/compiler-cli/test/main_spec.ts +++ b/packages/compiler-cli/test/main_spec.ts @@ -18,16 +18,22 @@ function getNgRootDir() { return moduleFilename.substr(0, distIndex); } -describe('compiler-cli', () => { +describe('compiler-cli with disableTransformerPipeline', () => { let basePath: string; let outDir: string; let write: (fileName: string, content: string) => void; + let errorSpy: jasmine.Spy&((s: string) => void); function writeConfig(tsconfig: string = '{"extends": "./tsconfig-base.json"}') { - write('tsconfig.json', tsconfig); + const json = JSON.parse(tsconfig); + // Note: 'extends' does not work for "angularCompilerOptions" yet. + const ngOptions = json['angularCompilerOptions'] = json['angularCompilerOptions'] || {}; + ngOptions['disableTransformerPipeline'] = true; + write('tsconfig.json', JSON.stringify(json)); } beforeEach(() => { + errorSpy = jasmine.createSpy('consoleError'); basePath = makeTempDir(); write = (fileName: string, content: string) => { fs.writeFileSync(path.join(basePath, fileName), content, {encoding: 'utf-8'}); @@ -58,13 +64,9 @@ describe('compiler-cli', () => { writeConfig(); write('test.ts', 'export const A = 1;'); - const mockConsole = {error: (s: string) => {}}; - - spyOn(mockConsole, 'error'); - - main({p: basePath}, mockConsole.error) + main(['-p', basePath], errorSpy) .then((exitCode) => { - expect(mockConsole.error).not.toHaveBeenCalled(); + expect(errorSpy).not.toHaveBeenCalled(); expect(exitCode).toEqual(0); done(); }) @@ -76,16 +78,11 @@ describe('compiler-cli', () => { "extends": "./tsconfig-base.json", "files": ["test.ts"] }`); - const mockConsole = {error: (s: string) => {}}; - spyOn(mockConsole, 'error'); - - main({p: basePath}, mockConsole.error) + main(['-p', basePath], errorSpy) .then((exitCode) => { - expect(mockConsole.error) - .toHaveBeenCalledWith( - `Error File '` + path.join(basePath, 'test.ts') + `' not found.`); - expect(mockConsole.error).not.toHaveBeenCalledWith('Compilation failed'); + expect(errorSpy).toHaveBeenCalledWith( + `Error File '` + path.join(basePath, 'test.ts') + `' not found.`); expect(exitCode).toEqual(1); done(); }) @@ -96,16 +93,10 @@ describe('compiler-cli', () => { writeConfig(); write('test.ts', 'foo;'); - const mockConsole = {error: (s: string) => {}}; - - spyOn(mockConsole, 'error'); - - main({p: basePath}, mockConsole.error) + main(['-p', basePath], errorSpy) .then((exitCode) => { - expect(mockConsole.error) - .toHaveBeenCalledWith( - 'Error at ' + path.join(basePath, 'test.ts') + `:1:1: Cannot find name 'foo'.`); - expect(mockConsole.error).not.toHaveBeenCalledWith('Compilation failed'); + expect(errorSpy).toHaveBeenCalledWith( + 'Error at ' + path.join(basePath, 'test.ts') + `:1:1: Cannot find name 'foo'.`); expect(exitCode).toEqual(1); done(); }) @@ -116,17 +107,11 @@ describe('compiler-cli', () => { writeConfig(); write('test.ts', `import {MyClass} from './not-exist-deps';`); - const mockConsole = {error: (s: string) => {}}; - - spyOn(mockConsole, 'error'); - - main({p: basePath}, mockConsole.error) + main(['-p', basePath], errorSpy) .then((exitCode) => { - expect(mockConsole.error) - .toHaveBeenCalledWith( - 'Error at ' + path.join(basePath, 'test.ts') + - `:1:23: Cannot find module './not-exist-deps'.`); - expect(mockConsole.error).not.toHaveBeenCalledWith('Compilation failed'); + expect(errorSpy).toHaveBeenCalledWith( + 'Error at ' + path.join(basePath, 'test.ts') + + `:1:23: Cannot find module './not-exist-deps'.`); expect(exitCode).toEqual(1); done(); }) @@ -138,17 +123,11 @@ describe('compiler-cli', () => { write('empty-deps.ts', 'export const A = 1;'); write('test.ts', `import {MyClass} from './empty-deps';`); - const mockConsole = {error: (s: string) => {}}; - - spyOn(mockConsole, 'error'); - - main({p: basePath}, mockConsole.error) + main(['-p', basePath], errorSpy) .then((exitCode) => { - expect(mockConsole.error) - .toHaveBeenCalledWith( - 'Error at ' + path.join(basePath, 'test.ts') + `:1:9: Module '"` + - path.join(basePath, 'empty-deps') + `"' has no exported member 'MyClass'.`); - expect(mockConsole.error).not.toHaveBeenCalledWith('Compilation failed'); + expect(errorSpy).toHaveBeenCalledWith( + 'Error at ' + path.join(basePath, 'test.ts') + `:1:9: Module '"` + + path.join(basePath, 'empty-deps') + `"' has no exported member 'MyClass'.`); expect(exitCode).toEqual(1); done(); }) @@ -163,18 +142,12 @@ describe('compiler-cli', () => { A(); `); - const mockConsole = {error: (s: string) => {}}; - - spyOn(mockConsole, 'error'); - - main({p: basePath}, mockConsole.error) + main(['-p', basePath], errorSpy) .then((exitCode) => { - expect(mockConsole.error) - .toHaveBeenCalledWith( - 'Error at ' + path.join(basePath, 'test.ts') + - ':3:7: Cannot invoke an expression whose type lacks a call signature. ' + - 'Type \'String\' has no compatible call signatures.'); - expect(mockConsole.error).not.toHaveBeenCalledWith('Compilation failed'); + expect(errorSpy).toHaveBeenCalledWith( + 'Error at ' + path.join(basePath, 'test.ts') + + ':3:7: Cannot invoke an expression whose type lacks a call signature. ' + + 'Type \'String\' has no compatible call signatures.'); expect(exitCode).toEqual(1); done(); }) @@ -184,14 +157,11 @@ describe('compiler-cli', () => { it('should print the stack trace on compiler internal errors', (done) => { write('test.ts', 'export const A = 1;'); - const mockConsole = {error: (s: string) => {}}; - - spyOn(mockConsole, 'error'); - - main({p: 'not-exist'}, mockConsole.error) + main(['-p', 'not-exist'], errorSpy) .then((exitCode) => { - expect(mockConsole.error).toHaveBeenCalled(); - expect(mockConsole.error).toHaveBeenCalledWith('Compilation failed'); + expect(errorSpy).toHaveBeenCalled(); + expect(errorSpy.calls.mostRecent().args[0]).toContain('no such file or directory'); + expect(errorSpy.calls.mostRecent().args[0]).toContain('at Error (native)'); expect(exitCode).toEqual(1); done(); }) @@ -215,7 +185,7 @@ describe('compiler-cli', () => { export class MyModule {} `); - main({p: basePath}) + main(['-p', basePath], errorSpy) .then((exitCode) => { expect(exitCode).toEqual(0); @@ -244,11 +214,7 @@ describe('compiler-cli', () => { export class MyModule {} `); - const mockConsole = {error: (s: string) => {}}; - - const errorSpy = spyOn(mockConsole, 'error'); - - main({p: basePath}, mockConsole.error) + main(['-p', basePath], errorSpy) .then((exitCode) => { expect(errorSpy).toHaveBeenCalledTimes(1); expect(errorSpy.calls.mostRecent().args[0]) @@ -280,7 +246,7 @@ describe('compiler-cli', () => { export class MyModule {} `); - main({p: basePath}) + main(['-p', basePath], errorSpy) .then((exitCode) => { expect(exitCode).toEqual(0); @@ -307,7 +273,7 @@ describe('compiler-cli', () => { export class MyModule {} `); - main({p: basePath}) + main(['-p', basePath], errorSpy) .then((exitCode) => { expect(exitCode).toEqual(0); expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngsummary.js'))).toBe(false); @@ -333,7 +299,7 @@ describe('compiler-cli', () => { export class MyModule {} `); - main({p: basePath}) + main(['-p', basePath], errorSpy) .then((exitCode) => { expect(exitCode).toEqual(0); expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngsummary.js'))).toBe(true); diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index 165582a09c..893068872b 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -11,8 +11,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; -import {main} from '../src/ngc'; -import {performCompilation, readConfiguration} from '../src/perform-compile'; +import {mainSync} from '../src/main'; function getNgRootDir() { const moduleFilename = module.filename.replace(/\\/g, '/'); @@ -20,16 +19,18 @@ function getNgRootDir() { return moduleFilename.substr(0, distIndex); } -describe('ngc command-line', () => { +describe('ngc transformer command-line', () => { let basePath: string; let outDir: string; let write: (fileName: string, content: string) => void; + let errorSpy: jasmine.Spy&((s: string) => void); function writeConfig(tsconfig: string = '{"extends": "./tsconfig-base.json"}') { write('tsconfig.json', tsconfig); } beforeEach(() => { + errorSpy = jasmine.createSpy('consoleError'); basePath = makeTempDir(); write = (fileName: string, content: string) => { const dir = path.dirname(fileName); @@ -66,35 +67,9 @@ describe('ngc command-line', () => { writeConfig(); write('test.ts', 'export const A = 1;'); - const mockConsole = {error: (s: string) => {}}; - - spyOn(mockConsole, 'error'); - - const result = main(['-p', basePath], mockConsole.error); - expect(mockConsole.error).not.toHaveBeenCalled(); - expect(result).toBe(0); - }); - - it('should be able to be called without a config file by passing options explicitly', () => { - write('test.ts', 'export const A = 1;'); - - const mockConsole = {error: (s: string) => {}}; - - spyOn(mockConsole, 'error'); - - expect( - () => performCompilation( - basePath, [path.join(basePath, 'test.ts')], { - experimentalDecorators: true, - skipLibCheck: true, - types: [], - outDir: path.join(basePath, 'built'), - declaration: true, - module: ts.ModuleKind.ES2015, - moduleResolution: ts.ModuleResolutionKind.NodeJs, - }, - {})) - .not.toThrow(); + const exitCode = mainSync(['-p', basePath], errorSpy); + expect(errorSpy).not.toHaveBeenCalled(); + expect(exitCode).toBe(0); }); it('should not print the stack trace if user input file does not exist', () => { @@ -102,16 +77,11 @@ describe('ngc command-line', () => { "extends": "./tsconfig-base.json", "files": ["test.ts"] }`); - const mockConsole = {error: (s: string) => {}}; - spyOn(mockConsole, 'error'); - - const exitCode = main(['-p', basePath], mockConsole.error); - expect(mockConsole.error) - .toHaveBeenCalledWith( - `error TS6053: File '` + path.join(basePath, 'test.ts') + `' not found.` + - '\n'); - expect(mockConsole.error).not.toHaveBeenCalledWith('Compilation failed'); + const exitCode = mainSync(['-p', basePath], errorSpy); + expect(errorSpy).toHaveBeenCalledWith( + `error TS6053: File '` + path.join(basePath, 'test.ts') + `' not found.` + + '\n'); expect(exitCode).toEqual(1); }); @@ -119,16 +89,10 @@ describe('ngc command-line', () => { writeConfig(); write('test.ts', 'foo;'); - const mockConsole = {error: (s: string) => {}}; - - spyOn(mockConsole, 'error'); - - const exitCode = main(['-p', basePath], mockConsole.error); - expect(mockConsole.error) - .toHaveBeenCalledWith( - `test.ts(1,1): error TS2304: Cannot find name 'foo'.` + - '\n'); - expect(mockConsole.error).not.toHaveBeenCalledWith('Compilation failed'); + const exitCode = mainSync(['-p', basePath], errorSpy); + expect(errorSpy).toHaveBeenCalledWith( + `test.ts(1,1): error TS2304: Cannot find name 'foo'.` + + '\n'); expect(exitCode).toEqual(1); }); @@ -136,16 +100,10 @@ describe('ngc command-line', () => { writeConfig(); write('test.ts', `import {MyClass} from './not-exist-deps';`); - const mockConsole = {error: (s: string) => {}}; - - spyOn(mockConsole, 'error'); - - const exitCode = main(['-p', basePath], mockConsole.error); - expect(mockConsole.error) - .toHaveBeenCalledWith( - `test.ts(1,23): error TS2307: Cannot find module './not-exist-deps'.` + - '\n'); - expect(mockConsole.error).not.toHaveBeenCalledWith('Compilation failed'); + const exitCode = mainSync(['-p', basePath], errorSpy); + expect(errorSpy).toHaveBeenCalledWith( + `test.ts(1,23): error TS2307: Cannot find module './not-exist-deps'.` + + '\n'); expect(exitCode).toEqual(1); }); @@ -154,17 +112,11 @@ describe('ngc command-line', () => { write('empty-deps.ts', 'export const A = 1;'); write('test.ts', `import {MyClass} from './empty-deps';`); - const mockConsole = {error: (s: string) => {}}; - - spyOn(mockConsole, 'error'); - - const exitCode = main(['-p', basePath], mockConsole.error); - expect(mockConsole.error) - .toHaveBeenCalledWith( - `test.ts(1,9): error TS2305: Module '"` + path.join(basePath, 'empty-deps') + - `"' has no exported member 'MyClass'.` + - '\n'); - expect(mockConsole.error).not.toHaveBeenCalledWith('Compilation failed'); + const exitCode = mainSync(['-p', basePath], errorSpy); + expect(errorSpy).toHaveBeenCalledWith( + `test.ts(1,9): error TS2305: Module '"` + path.join(basePath, 'empty-deps') + + `"' has no exported member 'MyClass'.` + + '\n'); expect(exitCode).toEqual(1); }); @@ -176,30 +128,21 @@ describe('ngc command-line', () => { A(); `); - const mockConsole = {error: (s: string) => {}}; - - spyOn(mockConsole, 'error'); - - const exitCode = main(['-p', basePath], mockConsole.error); - expect(mockConsole.error) - .toHaveBeenCalledWith( - 'test.ts(3,7): error TS2349: Cannot invoke an expression whose type lacks a call signature. ' + - 'Type \'String\' has no compatible call signatures.\n'); - expect(mockConsole.error).not.toHaveBeenCalledWith('Compilation failed'); + const exitCode = mainSync(['-p', basePath], errorSpy); + expect(errorSpy).toHaveBeenCalledWith( + 'test.ts(3,7): error TS2349: Cannot invoke an expression whose type lacks a call signature. ' + + 'Type \'String\' has no compatible call signatures.\n'); expect(exitCode).toEqual(1); }); it('should print the stack trace on compiler internal errors', () => { write('test.ts', 'export const A = 1;'); - const mockConsole = {error: (s: string) => {}}; - - spyOn(mockConsole, 'error'); - - const exitCode = main(['-p', 'not-exist'], mockConsole.error); - expect(mockConsole.error).toHaveBeenCalled(); - expect(mockConsole.error).toHaveBeenCalledWith('Compilation failed'); - expect(exitCode).toEqual(2); + const exitCode = mainSync(['-p', 'not-exist'], errorSpy); + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy.calls.mostRecent().args[0]).toContain('no such file or directory'); + expect(errorSpy.calls.mostRecent().args[0]).toContain('at Error (native)'); + expect(exitCode).toEqual(1); }); describe('compile ngfactory files', () => { @@ -218,11 +161,7 @@ describe('ngc command-line', () => { export class MyModule {} `); - const mockConsole = {error: (s: string) => {}}; - - const errorSpy = spyOn(mockConsole, 'error'); - - const exitCode = main(['-p', basePath], mockConsole.error); + const exitCode = mainSync(['-p', basePath], errorSpy); expect(errorSpy).toHaveBeenCalledTimes(1); expect(errorSpy.calls.mostRecent().args[0]) .toContain('Error at ng://' + path.join(basePath, 'mymodule.ts.MyComp.html')); @@ -253,11 +192,7 @@ describe('ngc command-line', () => { export class MyModule {} `); - const mockConsole = {error: (s: string) => {}}; - - const errorSpy = spyOn(mockConsole, 'error'); - - const exitCode = main(['-p', basePath], mockConsole.error); + const exitCode = mainSync(['-p', basePath], errorSpy); expect(errorSpy).toHaveBeenCalledTimes(1); expect(errorSpy.calls.mostRecent().args[0]) .toContain('Error at ng://' + path.join(basePath, 'my.component.html(1,5):')); @@ -282,7 +217,7 @@ describe('ngc command-line', () => { export class MyModule {} `); - const exitCode = main(['-p', basePath]); + const exitCode = mainSync(['-p', basePath], errorSpy); expect(exitCode).toEqual(0); expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngfactory.js'))).toBe(true); @@ -307,7 +242,7 @@ describe('ngc command-line', () => { export class MyModule {} `); - const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')]); + const exitCode = mainSync(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); expect(exitCode).toEqual(0); expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngfactory.js'))).toBe(true); expect(fs.existsSync(path.resolve( @@ -332,8 +267,7 @@ describe('ngc command-line', () => { export class MyModule {} `); - const mockConsole = {error: (s: string) => {}}; - const exitCode = main(['-p', basePath], mockConsole.error); + const exitCode = mainSync(['-p', basePath], errorSpy); expect(exitCode).toEqual(0); const mymodulejs = path.resolve(outDir, 'mymodule.js'); @@ -362,8 +296,7 @@ describe('ngc command-line', () => { export class MyModule {} `); - const mockConsole = {error: (s: string) => {}}; - const exitCode = main(['-p', basePath], mockConsole.error); + const exitCode = mainSync(['-p', basePath], errorSpy); expect(exitCode).toEqual(0); const mymodulejs = path.resolve(outDir, 'mymodule.js'); @@ -392,8 +325,7 @@ describe('ngc command-line', () => { export class MyModule {} `); - const mockConsole = {error: (s: string) => {}}; - const exitCode = main(['-p', basePath], mockConsole.error); + const exitCode = mainSync(['-p', basePath], errorSpy); expect(exitCode).toEqual(0); const mymodulejs = path.resolve(outDir, 'mymodule.js'); @@ -411,9 +343,9 @@ describe('ngc command-line', () => { }); function compile(): number { - const errors: string[] = []; - const result = main(['-p', path.join(basePath, 'tsconfig.json')], s => errors.push(s)); - expect(errors).toEqual([]); + errorSpy.calls.reset(); + const result = mainSync(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); + expect(errorSpy).not.toHaveBeenCalled(); return result; } @@ -607,68 +539,12 @@ describe('ngc command-line', () => { export class FlatModule { }`); - const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')]); + const exitCode = mainSync(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); expect(exitCode).toEqual(0); shouldExist('index.js'); shouldExist('index.metadata.json'); }); - it('should be able to build a flat module passing explicit options', () => { - write('public-api.ts', ` - export * from './src/flat.component'; - export * from './src/flat.module';`); - write('src/flat.component.html', '
flat module component
'); - write('src/flat.component.ts', ` - import {Component} from '@angular/core'; - - @Component({ - selector: 'flat-comp', - templateUrl: 'flat.component.html', - }) - export class FlatComponent { - }`); - write('src/flat.module.ts', ` - import {NgModule} from '@angular/core'; - - import {FlatComponent} from './flat.component'; - - @NgModule({ - declarations: [ - FlatComponent, - ], - exports: [ - FlatComponent, - ] - }) - export class FlatModule { - }`); - - const emitResult = performCompilation( - basePath, [path.join(basePath, 'public-api.ts')], { - target: ts.ScriptTarget.ES5, - experimentalDecorators: true, - noImplicitAny: true, - moduleResolution: ts.ModuleResolutionKind.NodeJs, - rootDir: basePath, - declaration: true, - lib: ['lib.es2015.d.ts', 'lib.dom.d.ts'], - baseUrl: basePath, - outDir: path.join(basePath, 'built'), - typeRoots: [path.join(basePath, 'node_modules/@types')] - }, - { - genDir: 'ng', - flatModuleId: 'flat_module', - flatModuleOutFile: 'index.js', - skipTemplateCodegen: true - }); - - - expect(emitResult.errorCode).toEqual(0); - shouldExist('index.js'); - shouldExist('index.metadata.json'); - }); - describe('with a third-party library', () => { const writeGenConfig = (skipCodegen: boolean) => { writeConfig(`{ @@ -756,7 +632,7 @@ describe('ngc command-line', () => { it('should honor skip code generation', () => { // First ensure that we skip code generation when requested;. writeGenConfig(/* skipCodegen */ true); - const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')]); + const exitCode = mainSync(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); expect(exitCode).toEqual(0); modules.forEach(moduleName => { shouldExist(moduleName + '.js'); @@ -772,7 +648,7 @@ describe('ngc command-line', () => { it('should produce factories', () => { // First ensure that we skip code generation when requested;. writeGenConfig(/* skipCodegen */ false); - const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')]); + const exitCode = mainSync(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); expect(exitCode).toEqual(0); modules.forEach(moduleName => { shouldExist(moduleName + '.js'); @@ -823,7 +699,7 @@ describe('ngc command-line', () => { }); it('should compile without error', () => { - expect(main(['-p', path.join(basePath, 'tsconfig.json')])).toBe(0); + expect(mainSync(['-p', path.join(basePath, 'tsconfig.json')], errorSpy)).toBe(0); }); }); @@ -897,7 +773,7 @@ describe('ngc command-line', () => { }); it('should be able to compile library 1', () => { - expect(main(['-p', path.join(basePath, 'lib1')])).toBe(0); + expect(mainSync(['-p', path.join(basePath, 'lib1')], errorSpy)).toBe(0); shouldExist('lib1/module.js'); shouldExist('lib1/module.ngsummary.json'); shouldExist('lib1/module.ngsummary.js'); @@ -907,8 +783,8 @@ describe('ngc command-line', () => { }); it('should be able to compile library 2', () => { - expect(main(['-p', path.join(basePath, 'lib1')])).toBe(0); - expect(main(['-p', path.join(basePath, 'lib2')])).toBe(0); + expect(mainSync(['-p', path.join(basePath, 'lib1')], errorSpy)).toBe(0); + expect(mainSync(['-p', path.join(basePath, 'lib2')], errorSpy)).toBe(0); shouldExist('lib2/module.js'); shouldExist('lib2/module.ngsummary.json'); shouldExist('lib2/module.ngsummary.js'); @@ -919,12 +795,12 @@ describe('ngc command-line', () => { describe('building an application', () => { beforeEach(() => { - expect(main(['-p', path.join(basePath, 'lib1')])).toBe(0); - expect(main(['-p', path.join(basePath, 'lib2')])).toBe(0); + expect(mainSync(['-p', path.join(basePath, 'lib1')], errorSpy)).toBe(0); + expect(mainSync(['-p', path.join(basePath, 'lib2')], errorSpy)).toBe(0); }); it('should build without error', () => { - expect(main(['-p', path.join(basePath, 'app')])).toBe(0); + expect(mainSync(['-p', path.join(basePath, 'app')], errorSpy)).toBe(0); shouldExist('app/main.js'); }); }); diff --git a/packages/compiler-cli/tsconfig-build.json b/packages/compiler-cli/tsconfig-build.json index 6937d14a26..3f1d3a822c 100644 --- a/packages/compiler-cli/tsconfig-build.json +++ b/packages/compiler-cli/tsconfig-build.json @@ -32,7 +32,6 @@ "files": [ "index.ts", "src/main.ts", - "src/ngc.ts", "src/extract_i18n.ts", "../../node_modules/@types/node/index.d.ts", "../../node_modules/@types/jasmine/index.d.ts", diff --git a/packages/tsc-wrapped/src/compiler_host.ts b/packages/tsc-wrapped/src/compiler_host.ts index eaff682940..47f7358dc7 100644 --- a/packages/tsc-wrapped/src/compiler_host.ts +++ b/packages/tsc-wrapped/src/compiler_host.ts @@ -120,51 +120,40 @@ export class MetadataWriterHost extends DelegatingHost { } } -export class SyntheticIndexHost extends DelegatingHost { - private normalSyntheticIndexName: string; - private indexContent: string; - private indexMetadata: string; +export function createSyntheticIndexHost( + delegate: H, syntheticIndex: {name: string, content: string, metadata: string}): H { + const normalSyntheticIndexName = normalize(syntheticIndex.name); + const indexContent = syntheticIndex.content; + const indexMetadata = syntheticIndex.metadata; - constructor( - delegate: ts.CompilerHost, - syntheticIndex: {name: string, content: string, metadata: string}) { - super(delegate); - this.normalSyntheticIndexName = normalize(syntheticIndex.name); - this.indexContent = syntheticIndex.content; - this.indexMetadata = syntheticIndex.metadata; - } + const newHost = Object.create(delegate); + newHost.fileExists = (fileName: string): boolean => { + return normalize(fileName) == normalSyntheticIndexName || delegate.fileExists(fileName); + }; - fileExists = (fileName: string): - boolean => { - return normalize(fileName) == this.normalSyntheticIndexName || - this.delegate.fileExists(fileName); - } + newHost.readFile = (fileName: string) => { + return normalize(fileName) == normalSyntheticIndexName ? indexContent : + delegate.readFile(fileName); + }; - readFile = - (fileName: string) => { - return normalize(fileName) == this.normalSyntheticIndexName ? - this.indexContent : - this.delegate.readFile(fileName); - } - - getSourceFile = - (fileName: string, languageVersion: ts.ScriptTarget, - onError?: (message: string) => void) => { - if (normalize(fileName) == this.normalSyntheticIndexName) { - return ts.createSourceFile(fileName, this.indexContent, languageVersion, true); + newHost.getSourceFile = + (fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void) => { + if (normalize(fileName) == normalSyntheticIndexName) { + return ts.createSourceFile(fileName, indexContent, languageVersion, true); } - return this.delegate.getSourceFile(fileName, languageVersion, onError); - } + return delegate.getSourceFile(fileName, languageVersion, onError); + }; - writeFile: ts.WriteFileCallback = - (fileName: string, data: string, writeByteOrderMark: boolean, - onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => { - this.delegate.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); - if (fileName.match(DTS) && sourceFiles && sourceFiles.length == 1 && - normalize(sourceFiles[0].fileName) == this.normalSyntheticIndexName) { - // If we are writing the synthetic index, write the metadata along side. - const metadataName = fileName.replace(DTS, '.metadata.json'); - writeFileSync(metadataName, this.indexMetadata, {encoding: 'utf8'}); - } - } + newHost.writeFile = + (fileName: string, data: string, writeByteOrderMark: boolean, + onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => { + delegate.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); + if (fileName.match(DTS) && sourceFiles && sourceFiles.length == 1 && + normalize(sourceFiles[0].fileName) == normalSyntheticIndexName) { + // If we are writing the synthetic index, write the metadata along side. + const metadataName = fileName.replace(DTS, '.metadata.json'); + writeFileSync(metadataName, indexMetadata, {encoding: 'utf8'}); + } + }; + return newHost; } \ No newline at end of file diff --git a/packages/tsc-wrapped/src/main.ts b/packages/tsc-wrapped/src/main.ts index 10a5cf0fbb..d2882da481 100644 --- a/packages/tsc-wrapped/src/main.ts +++ b/packages/tsc-wrapped/src/main.ts @@ -13,7 +13,7 @@ import * as ts from 'typescript'; import {CompilerHostAdapter, MetadataBundler} from './bundler'; import {CliOptions} from './cli_options'; -import {MetadataWriterHost, SyntheticIndexHost} from './compiler_host'; +import {MetadataWriterHost, createSyntheticIndexHost} from './compiler_host'; import {privateEntriesToIndex} from './index_writer'; import NgOptions from './options'; import {check, tsc} from './tsc'; @@ -33,9 +33,9 @@ export interface CodegenExtension { host: ts.CompilerHost): Promise; } -export function createBundleIndexHost( +export function createBundleIndexHost( ngOptions: NgOptions, rootFiles: string[], - host: ts.CompilerHost): {host: ts.CompilerHost, indexName?: string, errors?: ts.Diagnostic[]} { + host: H): {host: H, indexName?: string, errors?: ts.Diagnostic[]} { const files = rootFiles.filter(f => !DTS.test(f)); if (files.length != 1) { return { @@ -61,7 +61,7 @@ export function createBundleIndexHost( path.join(path.dirname(indexModule), ngOptions.flatModuleOutFile !.replace(JS_EXT, '.ts')); const libraryIndex = `./${path.basename(indexModule)}`; const content = privateEntriesToIndex(libraryIndex, metadataBundle.privates); - host = new SyntheticIndexHost(host, {name, content, metadata}); + host = createSyntheticIndexHost(host, {name, content, metadata}); return {host, indexName: name}; } diff --git a/tools/ngc-wrapped/index.ts b/tools/ngc-wrapped/index.ts index 0d32fce43b..3a6c0fb37c 100644 --- a/tools/ngc-wrapped/index.ts +++ b/tools/ngc-wrapped/index.ts @@ -9,26 +9,25 @@ // TODO(chuckj): Remove the requirement for a fake 'reflect` implementation from // the compiler import 'reflect-metadata'; -import {performCompilation} from '@angular/compiler-cli'; + +import {calcProjectFileAndBasePath, createNgCompilerOptions, formatDiagnostics, performCompilation} from '@angular/compiler-cli'; import * as fs from 'fs'; import * as path from 'path'; // Note, the tsc_wrapped module comes from rules_typescript, not from @angular/tsc-wrapped import {parseTsconfig} from 'tsc_wrapped'; +import * as ts from 'typescript'; function main(args: string[]) { - const [{options, bazelOpts, files, config}] = parseTsconfig(args[1]); - const ngOptions: {expectedOut: string[]} = (config as any).angularCompilerOptions; + const project = args[1]; + const [{options: tsOptions, bazelOpts, files, config}] = parseTsconfig(project); + const {basePath} = calcProjectFileAndBasePath(project); + const ngOptions = createNgCompilerOptions(basePath, config, tsOptions); - const parsedArgs = require('minimist')(args); - const project = parsedArgs.p || parsedArgs.project || '.'; - - const projectDir = fs.lstatSync(project).isFile() ? path.dirname(project) : project; - - // file names in tsconfig are resolved relative to this absolute path - const basePath = path.resolve(process.cwd(), projectDir); - const result = performCompilation(basePath, files, options, ngOptions, undefined); - - return result.errorCode; + const {diagnostics} = performCompilation(files, ngOptions); + if (diagnostics.length) { + console.error(formatDiagnostics(ngOptions, diagnostics)); + } + return diagnostics.some(d => d.category === ts.DiagnosticCategory.Error) ? 1 : 0; } if (require.main === module) { From 088532bf2e608ab5dce8b58eb37e8e8c644b4c50 Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Wed, 9 Aug 2017 16:39:49 -0700 Subject: [PATCH 19/83] perf(aio): update to new version of build-optimizer --- aio/scripts/_payload-limits.sh | 2 +- aio/yarn.lock | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/aio/scripts/_payload-limits.sh b/aio/scripts/_payload-limits.sh index b855e8e681..46f5338ba8 100755 --- a/aio/scripts/_payload-limits.sh +++ b/aio/scripts/_payload-limits.sh @@ -3,7 +3,7 @@ set -u -e -o pipefail declare -A limitUncompressed -limitUncompressed=(["inline"]=1600 ["main"]=600000 ["polyfills"]=35000) +limitUncompressed=(["inline"]=1600 ["main"]=550000 ["polyfills"]=35000) declare -A limitGzip7 limitGzip7=(["inline"]=1000 ["main"]=140000 ["polyfills"]=12500) declare -A limitGzip9 diff --git a/aio/yarn.lock b/aio/yarn.lock index 9e32cc0b32..c47e8cc0ee 100644 --- a/aio/yarn.lock +++ b/aio/yarn.lock @@ -2,12 +2,11 @@ # 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" +"@angular-devkit/build-optimizer@0.0.13": + version "0.0.13" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.0.13.tgz#cf397af76abe899aa909d4a735106694ca1f08cf" dependencies: loader-utils "^1.1.0" - magic-string "^0.19.1" source-map "^0.5.6" typescript "^2.3.3" @@ -27,7 +26,7 @@ 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" + "@angular-devkit/build-optimizer" "0.0.13" "@ngtools/json-schema" "1.1.0" "@ngtools/webpack" "1.6.0-rc.3" autoprefixer "^6.5.3" From d2c0d986d44f3a7a29bdeacbe973a5550f5c6839 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Fri, 28 Jul 2017 15:58:28 +0200 Subject: [PATCH 20/83] perf(core): add option to remove blank text nodes from compiled templates --- packages/compiler-cli/src/transformers/api.ts | 4 + .../compiler-cli/src/transformers/program.ts | 3 +- packages/compiler/src/aot/compiler.ts | 3 +- packages/compiler/src/aot/compiler_factory.ts | 1 + packages/compiler/src/aot/compiler_options.ts | 1 + packages/compiler/src/compile_metadata.ts | 10 +- packages/compiler/src/compiler.ts | 2 +- packages/compiler/src/config.ts | 17 ++- packages/compiler/src/directive_normalizer.ts | 15 ++- packages/compiler/src/directive_resolver.ts | 3 +- packages/compiler/src/jit/compiler.ts | 4 +- packages/compiler/src/jit/compiler_factory.ts | 3 + packages/compiler/src/metadata_resolver.ts | 6 +- .../src/ml_parser/html_whitespaces.ts | 83 ++++++++++++ packages/compiler/src/ml_parser/tags.ts | 7 ++ .../src/template_parser/template_parser.ts | 23 ++-- .../test/directive_normalizer_spec.ts | 54 +++++++- .../compiler/test/directive_resolver_spec.ts | 8 +- packages/compiler/test/integration_spec.ts | 1 - .../test/ml_parser/ast_serializer_spec.ts | 2 +- .../test/ml_parser/html_whitespaces_spec.ts | 118 ++++++++++++++++++ .../template_parser/template_parser_spec.ts | 68 ++++++++-- .../testing/src/directive_resolver_mock.ts | 3 +- packages/core/src/linker/compiler.ts | 1 + packages/core/src/metadata/directives.ts | 10 ++ packages/core/test/linker/integration_spec.ts | 45 +++++++ tools/public_api_guard/core/core.d.ts | 1 + 27 files changed, 450 insertions(+), 46 deletions(-) create mode 100644 packages/compiler/src/ml_parser/html_whitespaces.ts create mode 100644 packages/compiler/test/ml_parser/html_whitespaces_spec.ts diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index 2c463c1437..1d3b5b6d34 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -111,6 +111,10 @@ export interface CompilerOptions extends ts.CompilerOptions { i18nInFile?: string; // How to handle missing messages i18nInMissingTranslations?: 'error'|'warning'|'ignore'; + + // Whether to remove blank text nodes from compiled templates. It is `true` by default + // in Angular 5 and will be re-visited in Angular 6. + preserveWhitespaces?: boolean; } export interface ModuleFilenameResolver { diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index 9e1cc3a84c..5c079304ce 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -347,6 +347,7 @@ function getAotCompilerOptions(options: CompilerOptions): AotCompilerOptions { i18nFormat: options.i18nInFormat || options.i18nOutFormat, translations, missingTranslation, enableLegacyTemplate: options.enableLegacyTemplate, enableSummariesForJit: true, + preserveWhitespaces: options.preserveWhitespaces, }; } @@ -480,4 +481,4 @@ function createProgramWithStubsHost( fileExists = (fileName: string) => this.generatedFiles.has(fileName) || originalHost.fileExists(fileName); }; -} \ No newline at end of file +} diff --git a/packages/compiler/src/aot/compiler.ts b/packages/compiler/src/aot/compiler.ts index 03b1e34aac..9a4b870f90 100644 --- a/packages/compiler/src/aot/compiler.ts +++ b/packages/compiler/src/aot/compiler.ts @@ -322,9 +322,10 @@ export class AotCompiler { const pipes = ngModule.transitiveModule.pipes.map( pipe => this._metadataResolver.getPipeSummary(pipe.reference)); + const preserveWhitespaces = compMeta !.template !.preserveWhitespaces; const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse( compMeta, compMeta.template !.template !, directives, pipes, ngModule.schemas, - templateSourceUrl(ngModule.type, compMeta, compMeta.template !)); + templateSourceUrl(ngModule.type, compMeta, compMeta.template !), preserveWhitespaces); const stylesExpr = componentStyles ? o.variable(componentStyles.stylesVar) : o.literalArr([]); const viewResult = this._viewCompiler.compileComponent( outputCtx, compMeta, parsedTemplate, stylesExpr, usedPipes); diff --git a/packages/compiler/src/aot/compiler_factory.ts b/packages/compiler/src/aot/compiler_factory.ts index 7d911e4c81..cbdbc806ec 100644 --- a/packages/compiler/src/aot/compiler_factory.ts +++ b/packages/compiler/src/aot/compiler_factory.ts @@ -54,6 +54,7 @@ export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCom useJit: false, enableLegacyTemplate: options.enableLegacyTemplate !== false, missingTranslation: options.missingTranslation, + preserveWhitespaces: options.preserveWhitespaces, }); const normalizer = new DirectiveNormalizer( {get: (url: string) => compilerHost.loadResource(url)}, urlResolver, htmlParser, config); diff --git a/packages/compiler/src/aot/compiler_options.ts b/packages/compiler/src/aot/compiler_options.ts index ed260b3a6a..063c4d137b 100644 --- a/packages/compiler/src/aot/compiler_options.ts +++ b/packages/compiler/src/aot/compiler_options.ts @@ -15,4 +15,5 @@ export interface AotCompilerOptions { missingTranslation?: MissingTranslationStrategy; enableLegacyTemplate?: boolean; enableSummariesForJit?: boolean; + preserveWhitespaces?: boolean; } diff --git a/packages/compiler/src/compile_metadata.ts b/packages/compiler/src/compile_metadata.ts index 8d2b5c4f1e..9c62149604 100644 --- a/packages/compiler/src/compile_metadata.ts +++ b/packages/compiler/src/compile_metadata.ts @@ -252,8 +252,9 @@ export class CompileTemplateMetadata { animations: any[]; ngContentSelectors: string[]; interpolation: [string, string]|null; + preserveWhitespaces: boolean; constructor({encapsulation, template, templateUrl, styles, styleUrls, externalStylesheets, - animations, ngContentSelectors, interpolation, isInline}: { + animations, ngContentSelectors, interpolation, isInline, preserveWhitespaces}: { encapsulation: ViewEncapsulation | null, template: string|null, templateUrl: string|null, @@ -263,7 +264,8 @@ export class CompileTemplateMetadata { ngContentSelectors: string[], animations: any[], interpolation: [string, string]|null, - isInline: boolean + isInline: boolean, + preserveWhitespaces: boolean }) { this.encapsulation = encapsulation; this.template = template; @@ -278,6 +280,7 @@ export class CompileTemplateMetadata { } this.interpolation = interpolation; this.isInline = isInline; + this.preserveWhitespaces = preserveWhitespaces; } toSummary(): CompileTemplateSummary { @@ -516,7 +519,8 @@ export function createHostComponentMeta( animations: [], isInline: true, externalStylesheets: [], - interpolation: null + interpolation: null, + preserveWhitespaces: false, }), exportAs: null, changeDetection: ChangeDetectionStrategy.Default, diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index 8a88d720b9..f3f9fa87d8 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -24,7 +24,7 @@ export {VERSION} from './version'; export * from './template_parser/template_ast'; export {TEMPLATE_TRANSFORMS} from './template_parser/template_parser'; -export {CompilerConfig} from './config'; +export {CompilerConfig, preserveWhitespacesDefault} from './config'; export * from './compile_metadata'; export * from './aot/compiler_factory'; export * from './aot/compiler'; diff --git a/packages/compiler/src/config.ts b/packages/compiler/src/config.ts index 75be193731..8ab10926e5 100644 --- a/packages/compiler/src/config.ts +++ b/packages/compiler/src/config.ts @@ -6,11 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {InjectionToken, MissingTranslationStrategy, ViewEncapsulation, isDevMode} from '@angular/core'; - -import {CompileIdentifierMetadata} from './compile_metadata'; -import {Identifiers} from './identifiers'; - +import {MissingTranslationStrategy, ViewEncapsulation} from '@angular/core'; +import {noUndefined} from './util'; export class CompilerConfig { public defaultEncapsulation: ViewEncapsulation|null; @@ -19,18 +16,26 @@ export class CompilerConfig { public enableLegacyTemplate: boolean; public useJit: boolean; public missingTranslation: MissingTranslationStrategy|null; + public preserveWhitespaces: boolean; constructor( {defaultEncapsulation = ViewEncapsulation.Emulated, useJit = true, missingTranslation, - enableLegacyTemplate}: { + enableLegacyTemplate, preserveWhitespaces}: { defaultEncapsulation?: ViewEncapsulation, useJit?: boolean, missingTranslation?: MissingTranslationStrategy, enableLegacyTemplate?: boolean, + preserveWhitespaces?: boolean } = {}) { this.defaultEncapsulation = defaultEncapsulation; this.useJit = !!useJit; this.missingTranslation = missingTranslation || null; this.enableLegacyTemplate = enableLegacyTemplate !== false; + this.preserveWhitespaces = preserveWhitespacesDefault(noUndefined(preserveWhitespaces)); } } + +export function preserveWhitespacesDefault( + preserveWhitespacesOption: boolean | null, defaultSetting = true): boolean { + return preserveWhitespacesOption === null ? defaultSetting : preserveWhitespacesOption; +} diff --git a/packages/compiler/src/directive_normalizer.ts b/packages/compiler/src/directive_normalizer.ts index e9484a34ae..299263feeb 100644 --- a/packages/compiler/src/directive_normalizer.ts +++ b/packages/compiler/src/directive_normalizer.ts @@ -9,7 +9,7 @@ import {ViewEncapsulation, ɵstringify as stringify} from '@angular/core'; import {CompileAnimationEntryMetadata, CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, templateSourceUrl} from './compile_metadata'; -import {CompilerConfig} from './config'; +import {CompilerConfig, preserveWhitespacesDefault} from './config'; import {CompilerInjectable} from './injectable'; import * as html from './ml_parser/ast'; import {HtmlParser} from './ml_parser/html_parser'; @@ -31,6 +31,7 @@ export interface PrenormalizedTemplateMetadata { interpolation: [string, string]|null; encapsulation: ViewEncapsulation|null; animations: CompileAnimationEntryMetadata[]; + preserveWhitespaces: boolean|null; } @CompilerInjectable() @@ -82,6 +83,13 @@ export class DirectiveNormalizer { throw syntaxError( `No template specified for component ${stringify(prenormData.componentType)}`); } + + if (isDefined(prenormData.preserveWhitespaces) && + typeof prenormData.preserveWhitespaces !== 'boolean') { + throw syntaxError( + `The preserveWhitespaces option for component ${stringify(prenormData.componentType)} must be a boolean`); + } + return SyncAsync.then( this.normalizeTemplateOnly(prenormData), (result: CompileTemplateMetadata) => this.normalizeExternalStylesheets(result)); @@ -149,7 +157,9 @@ export class DirectiveNormalizer { ngContentSelectors: visitor.ngContentSelectors, animations: prenormData.animations, interpolation: prenormData.interpolation, isInline, - externalStylesheets: [] + externalStylesheets: [], + preserveWhitespaces: preserveWhitespacesDefault( + prenormData.preserveWhitespaces, this._config.preserveWhitespaces), }); } @@ -168,6 +178,7 @@ export class DirectiveNormalizer { animations: templateMeta.animations, interpolation: templateMeta.interpolation, isInline: templateMeta.isInline, + preserveWhitespaces: templateMeta.preserveWhitespaces, })); } diff --git a/packages/compiler/src/directive_resolver.ts b/packages/compiler/src/directive_resolver.ts index 2b1790305f..599dc26678 100644 --- a/packages/compiler/src/directive_resolver.ts +++ b/packages/compiler/src/directive_resolver.ts @@ -152,7 +152,8 @@ export class DirectiveResolver { styleUrls: directive.styleUrls, encapsulation: directive.encapsulation, animations: directive.animations, - interpolation: directive.interpolation + interpolation: directive.interpolation, + preserveWhitespaces: directive.preserveWhitespaces, }); } else { return new Directive({ diff --git a/packages/compiler/src/jit/compiler.ts b/packages/compiler/src/jit/compiler.ts index 7cf3a447b3..4a3fbf9430 100644 --- a/packages/compiler/src/jit/compiler.ts +++ b/packages/compiler/src/jit/compiler.ts @@ -262,6 +262,7 @@ export class JitCompiler implements Compiler { const externalStylesheetsByModuleUrl = new Map(); const outputContext = createOutputContext(); const componentStylesheet = this._styleCompiler.compileComponent(outputContext, compMeta); + const preserveWhitespaces = compMeta !.template !.preserveWhitespaces; compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => { const compiledStylesheet = this._styleCompiler.compileStyles(createOutputContext(), compMeta, stylesheetMeta); @@ -274,7 +275,8 @@ export class JitCompiler implements Compiler { pipe => this._metadataResolver.getPipeSummary(pipe.reference)); const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse( compMeta, compMeta.template !.template !, directives, pipes, template.ngModule.schemas, - templateSourceUrl(template.ngModule.type, template.compMeta, template.compMeta.template !)); + templateSourceUrl(template.ngModule.type, template.compMeta, template.compMeta.template !), + preserveWhitespaces); const compileResult = this._viewCompiler.compileComponent( outputContext, compMeta, parsedTemplate, ir.variable(componentStylesheet.stylesVar), usedPipes); diff --git a/packages/compiler/src/jit/compiler_factory.ts b/packages/compiler/src/jit/compiler_factory.ts index 1e51157f0d..b57b2bf0c8 100644 --- a/packages/compiler/src/jit/compiler_factory.ts +++ b/packages/compiler/src/jit/compiler_factory.ts @@ -123,6 +123,7 @@ export class JitCompilerFactory implements CompilerFactory { defaultEncapsulation: ViewEncapsulation.Emulated, missingTranslation: MissingTranslationStrategy.Warning, enableLegacyTemplate: true, + preserveWhitespaces: true, }; this._defaultOptions = [compilerOptions, ...defaultOptions]; @@ -142,6 +143,7 @@ export class JitCompilerFactory implements CompilerFactory { defaultEncapsulation: opts.defaultEncapsulation, missingTranslation: opts.missingTranslation, enableLegacyTemplate: opts.enableLegacyTemplate, + preserveWhitespaces: opts.preserveWhitespaces, }); }, deps: [] @@ -169,6 +171,7 @@ function _mergeOptions(optionsArr: CompilerOptions[]): CompilerOptions { providers: _mergeArrays(optionsArr.map(options => options.providers !)), missingTranslation: _lastDefined(optionsArr.map(options => options.missingTranslation)), enableLegacyTemplate: _lastDefined(optionsArr.map(options => options.enableLegacyTemplate)), + preserveWhitespaces: _lastDefined(optionsArr.map(options => options.preserveWhitespaces)), }; } diff --git a/packages/compiler/src/metadata_resolver.ts b/packages/compiler/src/metadata_resolver.ts index 5efad1d1d5..5a1a9e3f25 100644 --- a/packages/compiler/src/metadata_resolver.ts +++ b/packages/compiler/src/metadata_resolver.ts @@ -219,7 +219,8 @@ export class CompileMetadataResolver { styles: template.styles, styleUrls: template.styleUrls, animations: template.animations, - interpolation: template.interpolation + interpolation: template.interpolation, + preserveWhitespaces: template.preserveWhitespaces }); if (isPromise(templateMeta) && isSync) { this._reportError(componentStillLoadingError(directiveType), directiveType); @@ -267,7 +268,8 @@ export class CompileMetadataResolver { interpolation: noUndefined(dirMeta.interpolation), isInline: !!dirMeta.template, externalStylesheets: [], - ngContentSelectors: [] + ngContentSelectors: [], + preserveWhitespaces: noUndefined(dirMeta.preserveWhitespaces), }); } diff --git a/packages/compiler/src/ml_parser/html_whitespaces.ts b/packages/compiler/src/ml_parser/html_whitespaces.ts new file mode 100644 index 0000000000..f2cd01f7e3 --- /dev/null +++ b/packages/compiler/src/ml_parser/html_whitespaces.ts @@ -0,0 +1,83 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as html from './ast'; +import {ParseTreeResult} from './parser'; +import {NGSP_UNICODE} from './tags'; + +export const PRESERVE_WS_ATTR_NAME = 'ngPreserveWhitespaces'; + +const SKIP_WS_TRIM_TAGS = new Set(['pre', 'template', 'textarea', 'script', 'style']); + +function hasPreserveWhitespacesAttr(attrs: html.Attribute[]): boolean { + return attrs.some((attr: html.Attribute) => attr.name === PRESERVE_WS_ATTR_NAME); +} + +/** + * This visitor can walk HTML parse tree and remove / trim text nodes using the following rules: + * - consider spaces, tabs and new lines as whitespace characters; + * - drop text nodes consisting of whitespace characters only; + * - for all other text nodes replace consecutive whitespace characters with one space; + * - convert &ngsp; pseudo-entity to a single space; + * + * The idea of using &ngsp; as a placeholder for non-removable space was originally introduced in + * Angular Dart, see: + * https://github.com/dart-lang/angular/blob/0bb611387d29d65b5af7f9d2515ab571fd3fbee4/_tests/test/compiler/preserve_whitespace_test.dart#L25-L32 + * In Angular Dart &ngsp; is converted to the 0xE500 PUA (Private Use Areas) unicode character + * and later on replaced by a space. We are re-implementing the same idea here. + * + * Removal and trimming of whitespaces have positive performance impact (less code to generate + * while compiling templates, faster view creation). At the same time it can be "destructive" + * in some cases (whitespaces can influence layout). Becouse of the potential of breaking layout + * this visitor is not activated by default in Angular 5 and people need to explicitly opt-in for + * whitespace removal. The default option for whitespace removal will be revisited in Angular 6 + * and might be changed to "on" by default. + */ +class WhitespaceVisitor implements html.Visitor { + visitElement(element: html.Element, context: any): any { + if (SKIP_WS_TRIM_TAGS.has(element.name) || hasPreserveWhitespacesAttr(element.attrs)) { + // don't descent into elements where we need to preserve whitespaces + // but still visit all attributes to eliminate one used as a market to preserve WS + return new html.Element( + element.name, html.visitAll(this, element.attrs), element.children, element.sourceSpan, + element.startSourceSpan, element.endSourceSpan); + } + + return new html.Element( + element.name, element.attrs, html.visitAll(this, element.children), element.sourceSpan, + element.startSourceSpan, element.endSourceSpan); + } + + visitAttribute(attribute: html.Attribute, context: any): any { + return attribute.name !== PRESERVE_WS_ATTR_NAME ? attribute : null; + } + + visitText(text: html.Text, context: any): any { + const isBlank = text.value.trim().length === 0; + + if (!isBlank) { + // lexer is replacing the &ngsp; pseudo-entity with NGSP_UNICODE + return new html.Text( + text.value.replace(NGSP_UNICODE, ' ').replace(/\s\s+/g, ' '), text.sourceSpan); + } + + return null; + } + + visitComment(comment: html.Comment, context: any): any { return comment; } + + visitExpansion(expansion: html.Expansion, context: any): any { return expansion; } + + visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return expansionCase; } +} + +export function removeWhitespaces(htmlAstWithErrors: ParseTreeResult): ParseTreeResult { + return new ParseTreeResult( + html.visitAll(new WhitespaceVisitor(), htmlAstWithErrors.rootNodes), + htmlAstWithErrors.errors); +} diff --git a/packages/compiler/src/ml_parser/tags.ts b/packages/compiler/src/ml_parser/tags.ts index 5f06c3fd25..8fb6580d7c 100644 --- a/packages/compiler/src/ml_parser/tags.ts +++ b/packages/compiler/src/ml_parser/tags.ts @@ -71,6 +71,7 @@ export function mergeNsAndName(prefix: string, localName: string): string { // This list is not exhaustive to keep the compiler footprint low. // The `{` / `ƫ` syntax should be used when the named character reference does not // exist. + export const NAMED_ENTITIES: {[k: string]: string} = { 'Aacute': '\u00C1', 'aacute': '\u00E1', @@ -325,3 +326,9 @@ export const NAMED_ENTITIES: {[k: string]: string} = { 'zwj': '\u200D', 'zwnj': '\u200C', }; + +// The &ngsp; pseudo-entity is denoting a space. see: +// https://github.com/dart-lang/angular/blob/0bb611387d29d65b5af7f9d2515ab571fd3fbee4/_tests/test/compiler/preserve_whitespace_test.dart +export const NGSP_UNICODE = '\uE500'; + +NAMED_ENTITIES['ngsp'] = NGSP_UNICODE; diff --git a/packages/compiler/src/template_parser/template_parser.ts b/packages/compiler/src/template_parser/template_parser.ts index 9d342c2dda..ae5dab1455 100644 --- a/packages/compiler/src/template_parser/template_parser.ts +++ b/packages/compiler/src/template_parser/template_parser.ts @@ -18,6 +18,7 @@ import {Identifiers, createTokenForExternalReference, createTokenForReference} f import {CompilerInjectable} from '../injectable'; import * as html from '../ml_parser/ast'; import {ParseTreeResult} from '../ml_parser/html_parser'; +import {removeWhitespaces} from '../ml_parser/html_whitespaces'; import {expandNodes} from '../ml_parser/icu_ast_expander'; import {InterpolationConfig} from '../ml_parser/interpolation_config'; import {isNgTemplate, splitNsName} from '../ml_parser/tags'; @@ -113,9 +114,10 @@ export class TemplateParser { parse( component: CompileDirectiveMetadata, template: string, directives: CompileDirectiveSummary[], - pipes: CompilePipeSummary[], schemas: SchemaMetadata[], - templateUrl: string): {template: TemplateAst[], pipes: CompilePipeSummary[]} { - const result = this.tryParse(component, template, directives, pipes, schemas, templateUrl); + pipes: CompilePipeSummary[], schemas: SchemaMetadata[], templateUrl: string, + preserveWhitespaces: boolean): {template: TemplateAst[], pipes: CompilePipeSummary[]} { + const result = this.tryParse( + component, template, directives, pipes, schemas, templateUrl, preserveWhitespaces); const warnings = result.errors !.filter(error => error.level === ParseErrorLevel.WARNING) .filter(warnOnlyOnce( @@ -137,12 +139,17 @@ export class TemplateParser { tryParse( component: CompileDirectiveMetadata, template: string, directives: CompileDirectiveSummary[], - pipes: CompilePipeSummary[], schemas: SchemaMetadata[], - templateUrl: string): TemplateParseResult { + pipes: CompilePipeSummary[], schemas: SchemaMetadata[], templateUrl: string, + preserveWhitespaces: boolean): TemplateParseResult { + let htmlParseResult = this._htmlParser !.parse( + template, templateUrl, true, this.getInterpolationConfig(component)); + + if (!preserveWhitespaces) { + htmlParseResult = removeWhitespaces(htmlParseResult); + } + return this.tryParseHtml( - this.expandHtml(this._htmlParser !.parse( - template, templateUrl, true, this.getInterpolationConfig(component))), - component, directives, pipes, schemas); + this.expandHtml(htmlParseResult), component, directives, pipes, schemas); } tryParseHtml( diff --git a/packages/compiler/test/directive_normalizer_spec.ts b/packages/compiler/test/directive_normalizer_spec.ts index bd68115eb1..a2a1c06cc4 100644 --- a/packages/compiler/test/directive_normalizer_spec.ts +++ b/packages/compiler/test/directive_normalizer_spec.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ import {CompileAnimationEntryMetadata} from '@angular/compiler'; -import {CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, CompileTypeMetadata} from '@angular/compiler/src/compile_metadata'; -import {CompilerConfig} from '@angular/compiler/src/config'; +import {CompileStylesheetMetadata, CompileTemplateMetadata} from '@angular/compiler/src/compile_metadata'; +import {CompilerConfig, preserveWhitespacesDefault} from '@angular/compiler/src/config'; import {DirectiveNormalizer} from '@angular/compiler/src/directive_normalizer'; import {ResourceLoader} from '@angular/compiler/src/resource_loader'; import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock'; @@ -31,6 +31,7 @@ function normalizeTemplate(normalizer: DirectiveNormalizer, o: { interpolation?: [string, string] | null; encapsulation?: ViewEncapsulation | null; animations?: CompileAnimationEntryMetadata[]; + preserveWhitespaces?: boolean | null; }) { return normalizer.normalizeTemplate({ ngModuleType: noUndefined(o.ngModuleType), @@ -42,7 +43,8 @@ function normalizeTemplate(normalizer: DirectiveNormalizer, o: { styleUrls: noUndefined(o.styleUrls), interpolation: noUndefined(o.interpolation), encapsulation: noUndefined(o.encapsulation), - animations: noUndefined(o.animations) + animations: noUndefined(o.animations), + preserveWhitespaces: noUndefined(o.preserveWhitespaces), }); } @@ -54,6 +56,7 @@ function normalizeTemplateOnly(normalizer: DirectiveNormalizer, o: { interpolation?: [string, string] | null; encapsulation?: ViewEncapsulation | null; animations?: CompileAnimationEntryMetadata[]; + preserveWhitespaces?: boolean | null; }) { return normalizer.normalizeTemplateOnly({ ngModuleType: noUndefined(o.ngModuleType), @@ -65,13 +68,14 @@ function normalizeTemplateOnly(normalizer: DirectiveNormalizer, o: { styleUrls: noUndefined(o.styleUrls), interpolation: noUndefined(o.interpolation), encapsulation: noUndefined(o.encapsulation), - animations: noUndefined(o.animations) + animations: noUndefined(o.animations), + preserveWhitespaces: noUndefined(o.preserveWhitespaces), }); } function compileTemplateMetadata({encapsulation, template, templateUrl, styles, styleUrls, externalStylesheets, animations, ngContentSelectors, - interpolation, isInline}: { + interpolation, isInline, preserveWhitespaces}: { encapsulation?: ViewEncapsulation | null, template?: string | null, templateUrl?: string | null, @@ -81,7 +85,8 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles, ngContentSelectors?: string[], animations?: any[], interpolation?: [string, string] | null, - isInline?: boolean + isInline?: boolean, + preserveWhitespaces?: boolean | null }): CompileTemplateMetadata { return new CompileTemplateMetadata({ encapsulation: encapsulation || null, @@ -94,6 +99,7 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles, animations: animations || [], interpolation: interpolation || null, isInline: !!isInline, + preserveWhitespaces: preserveWhitespacesDefault(noUndefined(preserveWhitespaces)), }); } @@ -106,6 +112,7 @@ function normalizeLoadedTemplate( interpolation?: [string, string] | null; encapsulation?: ViewEncapsulation | null; animations?: CompileAnimationEntryMetadata[]; + preserveWhitespaces?: boolean; }, template: string, templateAbsUrl: string) { return normalizer.normalizeLoadedTemplate( @@ -120,6 +127,7 @@ function normalizeLoadedTemplate( interpolation: o.interpolation || null, encapsulation: o.encapsulation || null, animations: o.animations || [], + preserveWhitespaces: noUndefined(o.preserveWhitespaces), }, template, templateAbsUrl); } @@ -169,6 +177,18 @@ export function main() { })) .toThrowError(`'SomeComp' component cannot define both template and templateUrl`); })); + it('should throw if preserveWhitespaces is not a boolean', + inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { + expect(() => normalizeTemplate(normalizer, { + ngModuleType: null, + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + template: '', + preserveWhitespaces: 'WRONG', + })) + .toThrowError( + 'The preserveWhitespaces option for component SomeComp must be a boolean'); + })); }); describe('normalizeTemplateOnly sync', () => { @@ -431,6 +451,28 @@ export function main() { expect(template.encapsulation).toBe(viewEncapsulation); })); + it('should use preserveWhitespaces setting from compiler config if none provided', + inject( + [DirectiveNormalizer, CompilerConfig], + (normalizer: DirectiveNormalizer, config: CompilerConfig) => { + const template = normalizeLoadedTemplate(normalizer, {}, '', ''); + expect(template.preserveWhitespaces).toBe(config.preserveWhitespaces); + })); + + it('should store the preserveWhitespaces=false in the result', + inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { + const template = + normalizeLoadedTemplate(normalizer, {preserveWhitespaces: false}, '', ''); + expect(template.preserveWhitespaces).toBe(false); + })); + + it('should store the preserveWhitespaces=true in the result', + inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { + const template = + normalizeLoadedTemplate(normalizer, {preserveWhitespaces: true}, '', ''); + expect(template.preserveWhitespaces).toBe(true); + })); + it('should keep the template as html', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizeLoadedTemplate( diff --git a/packages/compiler/test/directive_resolver_spec.ts b/packages/compiler/test/directive_resolver_spec.ts index 7322277350..b98eb0eab0 100644 --- a/packages/compiler/test/directive_resolver_spec.ts +++ b/packages/compiler/test/directive_resolver_spec.ts @@ -79,7 +79,12 @@ class SomeDirectiveWithViewChild { c: any; } -@Component({selector: 'sample', template: 'some template', styles: ['some styles']}) +@Component({ + selector: 'sample', + template: 'some template', + styles: ['some styles'], + preserveWhitespaces: true +}) class ComponentWithTemplate { } @@ -439,6 +444,7 @@ export function main() { const compMetadata: Component = resolver.resolve(ComponentWithTemplate); expect(compMetadata.template).toEqual('some template'); expect(compMetadata.styles).toEqual(['some styles']); + expect(compMetadata.preserveWhitespaces).toBe(true); }); }); }); diff --git a/packages/compiler/test/integration_spec.ts b/packages/compiler/test/integration_spec.ts index c8c4e15e4a..321b37c0d9 100644 --- a/packages/compiler/test/integration_spec.ts +++ b/packages/compiler/test/integration_spec.ts @@ -9,7 +9,6 @@ import {Component, Directive, Input} from '@angular/core'; import {ComponentFixture, TestBed, async} from '@angular/core/testing'; import {By} from '@angular/platform-browser/src/dom/debug/by'; -import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {browserDetection} from '@angular/platform-browser/testing/src/browser_util'; import {expect} from '@angular/platform-browser/testing/src/matchers'; diff --git a/packages/compiler/test/ml_parser/ast_serializer_spec.ts b/packages/compiler/test/ml_parser/ast_serializer_spec.ts index 9452c3c9fb..5e33a302d5 100644 --- a/packages/compiler/test/ml_parser/ast_serializer_spec.ts +++ b/packages/compiler/test/ml_parser/ast_serializer_spec.ts @@ -97,4 +97,4 @@ const serializerVisitor = new _SerializerVisitor(); export function serializeNodes(nodes: html.Node[]): string[] { return nodes.map(node => node.visit(serializerVisitor, null)); -} \ No newline at end of file +} diff --git a/packages/compiler/test/ml_parser/html_whitespaces_spec.ts b/packages/compiler/test/ml_parser/html_whitespaces_spec.ts new file mode 100644 index 0000000000..7f9f6fa898 --- /dev/null +++ b/packages/compiler/test/ml_parser/html_whitespaces_spec.ts @@ -0,0 +1,118 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as html from '../../src/ml_parser/ast'; +import {HtmlParser} from '../../src/ml_parser/html_parser'; +import {PRESERVE_WS_ATTR_NAME, removeWhitespaces} from '../../src/ml_parser/html_whitespaces'; + +import {humanizeDom} from './ast_spec_utils'; + +export function main() { + describe('removeWhitespaces', () => { + + function parseAndRemoveWS(template: string): any[] { + return humanizeDom(removeWhitespaces(new HtmlParser().parse(template, 'TestComp'))); + } + + it('should remove blank text nodes', () => { + expect(parseAndRemoveWS(' ')).toEqual([]); + expect(parseAndRemoveWS('\n')).toEqual([]); + expect(parseAndRemoveWS('\t')).toEqual([]); + expect(parseAndRemoveWS(' \t \n ')).toEqual([]); + }); + + it('should remove whitespaces (space, tab, new line) between elements', () => { + expect(parseAndRemoveWS('

\t
\n
')).toEqual([ + [html.Element, 'br', 0], + [html.Element, 'br', 0], + [html.Element, 'br', 0], + [html.Element, 'br', 0], + ]); + }); + + it('should remove whitespaces from child text nodes', () => { + expect(parseAndRemoveWS('
')).toEqual([ + [html.Element, 'div', 0], + [html.Element, 'span', 1], + ]); + }); + + it('should remove whitespaces from the beginning and end of a template', () => { + expect(parseAndRemoveWS(`
\t`)).toEqual([ + [html.Element, 'br', 0], + ]); + }); + + it('should convert &ngsp; to a space and preserve it', () => { + expect(parseAndRemoveWS('
foo&ngsp;bar
')).toEqual([ + [html.Element, 'div', 0], + [html.Element, 'span', 1], + [html.Text, 'foo', 2], + [html.Text, ' ', 1], + [html.Element, 'span', 1], + [html.Text, 'bar', 2], + ]); + }); + + it('should replace multiple whitespaces with one space', () => { + expect(parseAndRemoveWS('\n\n\nfoo\t\t\t')).toEqual([[html.Text, ' foo ', 0]]); + expect(parseAndRemoveWS(' \n foo \t ')).toEqual([[html.Text, ' foo ', 0]]); + }); + + it('should not replace single tab and newline with spaces', () => { + expect(parseAndRemoveWS('\nfoo')).toEqual([[html.Text, '\nfoo', 0]]); + expect(parseAndRemoveWS('\tfoo')).toEqual([[html.Text, '\tfoo', 0]]); + }); + + it('should preserve single whitespaces between interpolations', () => { + expect(parseAndRemoveWS(`{{fooExp}} {{barExp}}`)).toEqual([ + [html.Text, '{{fooExp}} {{barExp}}', 0], + ]); + expect(parseAndRemoveWS(`{{fooExp}}\t{{barExp}}`)).toEqual([ + [html.Text, '{{fooExp}}\t{{barExp}}', 0], + ]); + expect(parseAndRemoveWS(`{{fooExp}}\n{{barExp}}`)).toEqual([ + [html.Text, '{{fooExp}}\n{{barExp}}', 0], + ]); + }); + + it('should preserve whitespaces around interpolations', () => { + expect(parseAndRemoveWS(` {{exp}} `)).toEqual([ + [html.Text, ' {{exp}} ', 0], + ]); + }); + + it('should preserve whitespaces inside
 elements', () => {
+      expect(parseAndRemoveWS(`
foo\nbar
`)).toEqual([ + [html.Element, 'pre', 0], + [html.Element, 'strong', 1], + [html.Text, 'foo', 2], + [html.Text, '\n', 1], + [html.Element, 'strong', 1], + [html.Text, 'bar', 2], + ]); + }); + + it('should skip whitespace trimming in `)).toEqual([ + [html.Element, 'textarea', 0], + [html.Text, 'foo\n\n bar', 1], + ]); + }); + + it(`should preserve whitespaces inside elements annotated with ${PRESERVE_WS_ATTR_NAME}`, + () => { + expect(parseAndRemoveWS(`
`)).toEqual([ + [html.Element, 'div', 0], + [html.Element, 'img', 1], + [html.Text, ' ', 1], + [html.Element, 'img', 1], + ]); + }); + }); +} diff --git a/packages/compiler/test/template_parser/template_parser_spec.ts b/packages/compiler/test/template_parser/template_parser_spec.ts index 8204b2f67c..7ffb4dfe58 100644 --- a/packages/compiler/test/template_parser/template_parser_spec.ts +++ b/packages/compiler/test/template_parser/template_parser_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 {CompileQueryMetadata, CompilerConfig, JitReflector, ProxyClass, StaticSymbol} from '@angular/compiler'; +import {CompileQueryMetadata, CompilerConfig, JitReflector, ProxyClass, StaticSymbol, preserveWhitespacesDefault} from '@angular/compiler'; import {CompileAnimationEntryMetadata, CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileTemplateMetadata, CompileTokenMetadata, CompileTypeMetadata, tokenReference} from '@angular/compiler/src/compile_metadata'; import {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry'; import {ElementSchemaRegistry} from '@angular/compiler/src/schema/element_schema_registry'; @@ -84,7 +84,7 @@ function compileDirectiveMetadataCreate( function compileTemplateMetadata({encapsulation, template, templateUrl, styles, styleUrls, externalStylesheets, animations, ngContentSelectors, - interpolation, isInline}: { + interpolation, isInline, preserveWhitespaces}: { encapsulation?: ViewEncapsulation | null, template?: string | null, templateUrl?: string | null, @@ -94,7 +94,8 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles, ngContentSelectors?: string[], animations?: any[], interpolation?: [string, string] | null, - isInline?: boolean + isInline?: boolean, + preserveWhitespaces?: boolean | null, }): CompileTemplateMetadata { return new CompileTemplateMetadata({ encapsulation: noUndefined(encapsulation), @@ -106,7 +107,8 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles, animations: animations || [], ngContentSelectors: ngContentSelectors || [], interpolation: noUndefined(interpolation), - isInline: !!isInline + isInline: !!isInline, + preserveWhitespaces: preserveWhitespacesDefault(noUndefined(preserveWhitespaces)), }); } @@ -116,7 +118,7 @@ export function main() { let ngIf: CompileDirectiveSummary; let parse: ( template: string, directives: CompileDirectiveSummary[], pipes?: CompilePipeSummary[], - schemas?: SchemaMetadata[]) => TemplateAst[]; + schemas?: SchemaMetadata[], preserveWhitespaces?: boolean) => TemplateAst[]; let console: ArrayConsole; function commonBeforeEach() { @@ -148,12 +150,15 @@ export function main() { parse = (template: string, directives: CompileDirectiveSummary[], - pipes: CompilePipeSummary[] | null = null, - schemas: SchemaMetadata[] = []): TemplateAst[] => { + pipes: CompilePipeSummary[] | null = null, schemas: SchemaMetadata[] = [], + preserveWhitespaces = true): TemplateAst[] => { if (pipes === null) { pipes = []; } - return parser.parse(component, template, directives, pipes, schemas, 'TestComp') + return parser + .parse( + component, template, directives, pipes, schemas, 'TestComp', + preserveWhitespaces) .template; }; })); @@ -398,7 +403,8 @@ export function main() { externalStylesheets: [], styleUrls: [], styles: [], - encapsulation: null + encapsulation: null, + preserveWhitespaces: preserveWhitespacesDefault(null), }), isHost: false, exportAs: null, @@ -417,7 +423,7 @@ export function main() { }); expect(humanizeTplAst( - parser.parse(component, '{%a%}', [], [], [], 'TestComp').template, + parser.parse(component, '{%a%}', [], [], [], 'TestComp', true).template, {start: '{%', end: '%}'})) .toEqual([[BoundTextAst, '{% a %}']]); })); @@ -2052,6 +2058,48 @@ The pipe 'test' could not be found ("{{[ERROR ->]a | test}}"): TestComp@0:2`); }); }); + describe('whitespaces removal', () => { + + it('should not remove whitespaces by default', () => { + expect(humanizeTplAst(parse('

\t
\n
', []))).toEqual([ + [TextAst, ' '], + [ElementAst, 'br'], + [TextAst, ' '], + [ElementAst, 'br'], + [TextAst, '\t'], + [ElementAst, 'br'], + [TextAst, '\n'], + [ElementAst, 'br'], + [TextAst, ' '], + ]); + }); + + it('should remove whitespaces when explicitly requested', () => { + expect(humanizeTplAst(parse('

\t
\n
', [], [], [], false))).toEqual([ + [ElementAst, 'br'], + [ElementAst, 'br'], + [ElementAst, 'br'], + [ElementAst, 'br'], + ]); + }); + + it('should remove whitespace between ICU expansions when not preserving whitespaces', () => { + const shortForm = '{ count, plural, =0 {small} many {big} }'; + const expandedForm = '' + + 'small' + + 'big' + + ''; + const humanizedExpandedForm = humanizeTplAst(parse(expandedForm, [])); + + // ICU expansions are converted to `` tags and all blank text nodes are reomved + // so any whitespace between ICU exansions are removed as well + expect(humanizeTplAst(parse(`${shortForm} ${shortForm}`, [], [], [], false))).toEqual([ + ...humanizedExpandedForm, ...humanizedExpandedForm + ]); + }); + + }); + describe('Template Parser - opt-out `