963 lines
36 KiB
TypeScript
963 lines
36 KiB
TypeScript
/**
|
|
* @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 {AotCompiler, AotCompilerHost, AotCompilerOptions, GeneratedFile, createAotCompiler} from '@angular/compiler';
|
|
import {RenderComponentType, ɵReflectionCapabilities as ReflectionCapabilities, ɵreflector as reflector} from '@angular/core';
|
|
import {NodeFlags} from '@angular/core/src/view/index';
|
|
import {async} from '@angular/core/testing';
|
|
import {MetadataBundler, MetadataCollector, ModuleMetadata, privateEntriesToIndex} from '@angular/tsc-wrapped';
|
|
import * as ts from 'typescript';
|
|
|
|
import {extractSourceMap, originalPositionFor} from '../output/source_map_util';
|
|
|
|
import {EmittingCompilerHost, MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, MockMetadataBundlerHost, settings} from './test_util';
|
|
|
|
const DTS = /\.d\.ts$/;
|
|
|
|
const minCoreIndex = `
|
|
export * from './src/application_module';
|
|
export * from './src/change_detection';
|
|
export * from './src/metadata';
|
|
export * from './src/di/metadata';
|
|
export * from './src/di/injector';
|
|
export * from './src/di/injection_token';
|
|
export * from './src/linker';
|
|
export * from './src/render';
|
|
export * from './src/codegen_private_exports';
|
|
`;
|
|
|
|
describe('compiler (unbundled Angular)', () => {
|
|
let angularFiles: Map<string, string>;
|
|
|
|
beforeAll(() => {
|
|
const emittingHost = new EmittingCompilerHost([], {emitMetadata: true});
|
|
emittingHost.addScript('@angular/core/index.ts', minCoreIndex);
|
|
const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost);
|
|
emittingProgram.emit();
|
|
|
|
angularFiles = emittingHost.written;
|
|
});
|
|
|
|
// Restore reflector since AoT compiler will update it with a new static reflector
|
|
afterEach(() => { reflector.updateCapabilities(new ReflectionCapabilities()); });
|
|
|
|
describe('Quickstart', () => {
|
|
let host: MockCompilerHost;
|
|
let aotHost: MockAotCompilerHost;
|
|
|
|
beforeEach(() => {
|
|
host = new MockCompilerHost(QUICKSTART, FILES, angularFiles);
|
|
aotHost = new MockAotCompilerHost(host);
|
|
});
|
|
|
|
it('should compile',
|
|
async(() => compile(host, aotHost, expectNoDiagnostics).then(generatedFiles => {
|
|
expect(generatedFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl)))
|
|
.toBeDefined();
|
|
expect(generatedFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl)))
|
|
.toBeDefined();
|
|
})));
|
|
|
|
it('should compile using summaries',
|
|
async(() => summaryCompile(host, aotHost).then(generatedFiles => {
|
|
expect(generatedFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl)))
|
|
.toBeDefined();
|
|
expect(generatedFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl)))
|
|
.toBeDefined();
|
|
})));
|
|
});
|
|
|
|
describe('aot source mapping', () => {
|
|
const componentPath = '/app/app.component.ts';
|
|
const ngComponentPath = 'ng:///app/app.component.ts'
|
|
|
|
let rootDir: MockDirectory;
|
|
let appDir: MockDirectory;
|
|
|
|
beforeEach(() => {
|
|
appDir = {
|
|
'app.module.ts': `
|
|
import { NgModule } from '@angular/core';
|
|
|
|
import { AppComponent } from './app.component';
|
|
|
|
@NgModule({
|
|
declarations: [ AppComponent ],
|
|
bootstrap: [ AppComponent ]
|
|
})
|
|
export class AppModule { }
|
|
`
|
|
};
|
|
rootDir = {'app': appDir};
|
|
});
|
|
|
|
function compileApp(): Promise<GeneratedFile> {
|
|
return new Promise((resolve, reject) => {
|
|
const host = new MockCompilerHost(['/app/app.module.ts'], rootDir, angularFiles);
|
|
const aotHost = new MockAotCompilerHost(host);
|
|
let result: GeneratedFile[];
|
|
let error: Error;
|
|
resolve(compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics)
|
|
.then(
|
|
(files) => files.find(
|
|
genFile => genFile.srcFileUrl === componentPath &&
|
|
genFile.genFileUrl.endsWith('.ts'))));
|
|
});
|
|
}
|
|
|
|
function findLineAndColumn(file: string, token: string): {line: number, column: number} {
|
|
const index = file.indexOf(token);
|
|
if (index === -1) {
|
|
return {line: null, column: null};
|
|
}
|
|
const linesUntilToken = file.slice(0, index).split('\n');
|
|
const line = linesUntilToken.length;
|
|
const column = linesUntilToken[linesUntilToken.length - 1].length;
|
|
return {line, column};
|
|
}
|
|
|
|
function createComponentSource(componentDecorator: string) {
|
|
return `
|
|
import { NgModule, Component } from '@angular/core';
|
|
|
|
@Component({
|
|
${componentDecorator}
|
|
})
|
|
export class AppComponent {
|
|
someMethod() {}
|
|
}
|
|
`;
|
|
}
|
|
|
|
describe('inline templates', () => {
|
|
const ngUrl = `${ngComponentPath}.AppComponent.html`;
|
|
|
|
function templateDecorator(template: string) { return `template: \`${template}\`,`; }
|
|
|
|
declareTests({ngUrl, templateDecorator});
|
|
});
|
|
|
|
describe('external templates', () => {
|
|
const ngUrl = 'ng:///app/app.component.html';
|
|
const templateUrl = '/app/app.component.html';
|
|
|
|
function templateDecorator(template: string) {
|
|
appDir['app.component.html'] = template;
|
|
return `templateUrl: 'app.component.html',`;
|
|
}
|
|
|
|
declareTests({ngUrl, templateDecorator});
|
|
});
|
|
|
|
function declareTests({ngUrl, templateDecorator}:
|
|
{ngUrl: string, templateDecorator: (template: string) => string}) {
|
|
it('should use the right source url in html parse errors', async(() => {
|
|
appDir['app.component.ts'] =
|
|
createComponentSource(templateDecorator('<div>\n </error>'));
|
|
|
|
expectPromiseToThrow(
|
|
compileApp(), new RegExp(`Template parse errors[\\s\\S]*${ngUrl}@1:2`));
|
|
}));
|
|
|
|
it('should use the right source url in template parse errors', async(() => {
|
|
appDir['app.component.ts'] = createComponentSource(
|
|
templateDecorator('<div>\n <div unknown="{{ctxProp}}"></div>'));
|
|
|
|
expectPromiseToThrow(
|
|
compileApp(), new RegExp(`Template parse errors[\\s\\S]*${ngUrl}@1:7`));
|
|
}));
|
|
|
|
it('should create a sourceMap for the template', async(() => {
|
|
const template = 'Hello World!';
|
|
|
|
appDir['app.component.ts'] = createComponentSource(templateDecorator(template));
|
|
|
|
compileApp().then((genFile) => {
|
|
const sourceMap = extractSourceMap(genFile.source);
|
|
expect(sourceMap.file).toEqual(genFile.genFileUrl);
|
|
|
|
// the generated file contains code that is not mapped to
|
|
// the template but rather to the original source file (e.g. import statements, ...)
|
|
const templateIndex = sourceMap.sources.indexOf(ngUrl);
|
|
expect(sourceMap.sourcesContent[templateIndex]).toEqual(template);
|
|
|
|
// for the mapping to the original source file we don't store the source code
|
|
// as we want to keep whatever TypeScript / ... produced for them.
|
|
const sourceIndex = sourceMap.sources.indexOf(ngComponentPath);
|
|
expect(sourceMap.sourcesContent[sourceIndex]).toBe(' ');
|
|
});
|
|
}));
|
|
|
|
it('should map elements correctly to the source', async(() => {
|
|
const template = '<div>\n <span></span></div>';
|
|
|
|
appDir['app.component.ts'] = createComponentSource(templateDecorator(template));
|
|
|
|
compileApp().then((genFile) => {
|
|
const sourceMap = extractSourceMap(genFile.source);
|
|
expect(originalPositionFor(sourceMap, findLineAndColumn(genFile.source, `'span'`)))
|
|
.toEqual({line: 2, column: 3, source: ngUrl});
|
|
});
|
|
}));
|
|
|
|
it('should map bindings correctly to the source', async(() => {
|
|
const template = `<div>\n <span [title]="someMethod()"></span></div>`;
|
|
|
|
appDir['app.component.ts'] = createComponentSource(templateDecorator(template));
|
|
|
|
compileApp().then((genFile) => {
|
|
const sourceMap = extractSourceMap(genFile.source);
|
|
expect(
|
|
originalPositionFor(sourceMap, findLineAndColumn(genFile.source, `someMethod()`)))
|
|
.toEqual({line: 2, column: 9, source: ngUrl});
|
|
});
|
|
}));
|
|
|
|
it('should map events correctly to the source', async(() => {
|
|
const template = `<div>\n <span (click)="someMethod()"></span></div>`;
|
|
|
|
appDir['app.component.ts'] = createComponentSource(templateDecorator(template));
|
|
|
|
compileApp().then((genFile) => {
|
|
const sourceMap = extractSourceMap(genFile.source);
|
|
expect(
|
|
originalPositionFor(sourceMap, findLineAndColumn(genFile.source, `someMethod()`)))
|
|
.toEqual({line: 2, column: 9, source: ngUrl});
|
|
});
|
|
}));
|
|
|
|
it('should map non template parts to the source file', async(() => {
|
|
appDir['app.component.ts'] = createComponentSource(templateDecorator('Hello World!'));
|
|
|
|
compileApp().then((genFile) => {
|
|
const sourceMap = extractSourceMap(genFile.source);
|
|
expect(originalPositionFor(sourceMap, {line: 1, column: 0}))
|
|
.toEqual({line: 1, column: 0, source: ngComponentPath});
|
|
});
|
|
}));
|
|
}
|
|
});
|
|
|
|
describe('errors', () => {
|
|
it('should only warn if not all arguments of an @Injectable class can be resolved',
|
|
async(() => {
|
|
const FILES: MockData = {
|
|
app: {
|
|
'app.ts': `
|
|
import {Injectable} from '@angular/core';
|
|
|
|
@Injectable()
|
|
export class MyService {
|
|
constructor(a: boolean) {}
|
|
}
|
|
`
|
|
}
|
|
};
|
|
const host = new MockCompilerHost(['/app/app.ts'], FILES, angularFiles);
|
|
const aotHost = new MockAotCompilerHost(host);
|
|
const warnSpy = spyOn(console, 'warn');
|
|
compile(host, aotHost, expectNoDiagnostics).then(() => {
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
`Warning: Can't resolve all parameters for MyService in /app/app.ts: (?). This will become an error in Angular v5.x`);
|
|
});
|
|
|
|
}));
|
|
});
|
|
|
|
it('should add the preamble to generated files', async(() => {
|
|
const FILES: MockData = {
|
|
app: {
|
|
'app.ts': `
|
|
import { NgModule, Component } from '@angular/core';
|
|
|
|
@Component({ template: '' })
|
|
export class AppComponent {}
|
|
|
|
@NgModule({ declarations: [ AppComponent ] })
|
|
export class AppModule { }
|
|
`
|
|
}
|
|
};
|
|
const host = new MockCompilerHost(['/app/app.ts'], FILES, angularFiles);
|
|
const aotHost = new MockAotCompilerHost(host);
|
|
const genFilePreamble = '/* Hello world! */';
|
|
compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics, {genFilePreamble})
|
|
.then((generatedFiles) => {
|
|
const genFile = generatedFiles.find(
|
|
gf => gf.srcFileUrl === '/app/app.ts' && gf.genFileUrl.endsWith('.ts'));
|
|
expect(genFile.source.startsWith(genFilePreamble)).toBe(true);
|
|
});
|
|
|
|
}));
|
|
|
|
describe('ComponentFactories', () => {
|
|
it('should include inputs, outputs and ng-content selectors in the component factory',
|
|
async(() => {
|
|
const FILES: MockData = {
|
|
app: {
|
|
'app.ts': `
|
|
import {Component, NgModule, Input, Output} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'my-comp',
|
|
template: '<ng-content></ng-content><ng-content select="child"></ng-content>'
|
|
})
|
|
export class MyComp {
|
|
@Input('aInputName')
|
|
aInputProp: string;
|
|
|
|
@Output('aOutputName')
|
|
aOutputProp: any;
|
|
}
|
|
|
|
@NgModule({
|
|
declarations: [MyComp]
|
|
})
|
|
export class MyModule {}
|
|
`
|
|
}
|
|
};
|
|
const host = new MockCompilerHost(['/app/app.ts'], FILES, angularFiles);
|
|
const aotHost = new MockAotCompilerHost(host);
|
|
let generatedFiles: GeneratedFile[];
|
|
compile(host, aotHost, expectNoDiagnostics).then((generatedFiles) => {
|
|
const genFile = generatedFiles.find(genFile => genFile.srcFileUrl === '/app/app.ts');
|
|
const createComponentFactoryCall =
|
|
/ɵccf\([^)]*\)/m.exec(genFile.source)[0].replace(/\s*/g, '');
|
|
// selector
|
|
expect(createComponentFactoryCall).toContain('my-comp');
|
|
// inputs
|
|
expect(createComponentFactoryCall).toContain(`{aInputProp:'aInputName'}`);
|
|
// outputs
|
|
expect(createComponentFactoryCall).toContain(`{aOutputProp:'aOutputName'}`);
|
|
// ngContentSelectors
|
|
expect(createComponentFactoryCall).toContain(`['*','child']`);
|
|
});
|
|
}));
|
|
});
|
|
|
|
describe('generated templates', () => {
|
|
it('should not call `check` for directives without bindings nor ngDoCheck/ngOnInit',
|
|
async(() => {
|
|
const FILES: MockData = {
|
|
app: {
|
|
'app.ts': `
|
|
import { NgModule, Component } from '@angular/core';
|
|
|
|
@Component({ template: '' })
|
|
export class AppComponent {}
|
|
|
|
@NgModule({ declarations: [ AppComponent ] })
|
|
export class AppModule { }
|
|
`
|
|
}
|
|
};
|
|
const host = new MockCompilerHost(['/app/app.ts'], FILES, angularFiles);
|
|
const aotHost = new MockAotCompilerHost(host);
|
|
const genFilePreamble = '/* Hello world! */';
|
|
compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics, {genFilePreamble})
|
|
.then((generatedFiles) => {
|
|
const genFile = generatedFiles.find(
|
|
gf => gf.srcFileUrl === '/app/app.ts' && gf.genFileUrl.endsWith('.ts'));
|
|
expect(genFile.source).not.toContain('check(');
|
|
});
|
|
|
|
}));
|
|
});
|
|
|
|
describe('inheritance with summaries', () => {
|
|
function compileWithSummaries(
|
|
libInput: MockData, appInput: MockData): Promise<GeneratedFile[]> {
|
|
const libHost = new MockCompilerHost(['/lib/base.ts'], libInput, angularFiles);
|
|
const libAotHost = new MockAotCompilerHost(libHost);
|
|
libAotHost.tsFilesOnly();
|
|
const appHost = new MockCompilerHost(['/app/main.ts'], appInput, angularFiles);
|
|
const appAotHost = new MockAotCompilerHost(appHost);
|
|
appAotHost.tsFilesOnly();
|
|
return compile(libHost, libAotHost, expectNoDiagnostics, expectNoDiagnosticsAndEmit)
|
|
.then(() => {
|
|
libHost.writtenFiles.forEach((value, key) => appHost.writeFile(key, value, false));
|
|
libHost.overrides.forEach((value, key) => appHost.override(key, value));
|
|
|
|
return compile(appHost, appAotHost, expectNoDiagnostics, expectNoDiagnosticsAndEmit);
|
|
});
|
|
}
|
|
|
|
function compileParentAndChild(
|
|
{parentClassDecorator, parentModuleDecorator, childClassDecorator, childModuleDecorator}: {
|
|
parentClassDecorator: string,
|
|
parentModuleDecorator: string,
|
|
childClassDecorator: string,
|
|
childModuleDecorator: string
|
|
}) {
|
|
const libInput: MockData = {
|
|
'lib': {
|
|
'base.ts': `
|
|
import {Injectable, Pipe, Directive, Component, NgModule} from '@angular/core';
|
|
|
|
${parentClassDecorator}
|
|
export class Base {}
|
|
|
|
${parentModuleDecorator}
|
|
export class BaseModule {}
|
|
`
|
|
}
|
|
};
|
|
const appInput: MockData = {
|
|
'app': {
|
|
'main.ts': `
|
|
import {Injectable, Pipe, Directive, Component, NgModule} from '@angular/core';
|
|
import {Base} from '../lib/base';
|
|
|
|
${childClassDecorator}
|
|
export class Extends extends Base {}
|
|
|
|
${childModuleDecorator}
|
|
export class MyModule {}
|
|
`
|
|
}
|
|
};
|
|
|
|
return compileWithSummaries(libInput, appInput)
|
|
.then((generatedFiles) => generatedFiles.find(gf => gf.srcFileUrl === '/app/main.ts'));
|
|
}
|
|
|
|
it('should inherit ctor and lifecycle hooks from classes in other compilation units',
|
|
async(() => {
|
|
const libInput: MockData = {
|
|
'lib': {
|
|
'base.ts': `
|
|
export class AParam {}
|
|
|
|
export class Base {
|
|
constructor(a: AParam) {}
|
|
ngOnDestroy() {}
|
|
}
|
|
`
|
|
}
|
|
};
|
|
const appInput: MockData = {
|
|
'app': {
|
|
'main.ts': `
|
|
import {NgModule, Component} from '@angular/core';
|
|
import {Base} from '../lib/base';
|
|
|
|
@Component({template: ''})
|
|
export class Extends extends Base {}
|
|
|
|
@NgModule({
|
|
declarations: [Extends]
|
|
})
|
|
export class MyModule {}
|
|
`
|
|
}
|
|
};
|
|
|
|
compileWithSummaries(libInput, appInput).then((generatedFiles) => {
|
|
const mainNgFactory = generatedFiles.find(gf => gf.srcFileUrl === '/app/main.ts');
|
|
const flags = NodeFlags.TypeDirective | NodeFlags.Component | NodeFlags.OnDestroy;
|
|
expect(mainNgFactory.source)
|
|
.toContain(`${flags},(null as any),0,import1.Extends,[import2.AParam]`);
|
|
});
|
|
}));
|
|
|
|
it('should inherit ctor and lifecycle hooks from classes in other compilation units over 2 levels',
|
|
async(() => {
|
|
const lib1Input: MockData = {
|
|
'lib1': {
|
|
'base.ts': `
|
|
export class AParam {}
|
|
|
|
export class Base {
|
|
constructor(a: AParam) {}
|
|
ngOnDestroy() {}
|
|
}
|
|
`
|
|
}
|
|
};
|
|
|
|
const lib2Input: MockData = {
|
|
'lib2': {
|
|
'middle.ts': `
|
|
import {Base} from '../lib1/base';
|
|
export class Middle extends Base {}
|
|
`
|
|
}
|
|
};
|
|
|
|
|
|
const appInput: MockData = {
|
|
'app': {
|
|
'main.ts': `
|
|
import {NgModule, Component} from '@angular/core';
|
|
import {Middle} from '../lib2/middle';
|
|
|
|
@Component({template: ''})
|
|
export class Extends extends Middle {}
|
|
|
|
@NgModule({
|
|
declarations: [Extends]
|
|
})
|
|
export class MyModule {}
|
|
`
|
|
}
|
|
};
|
|
const lib1Host = new MockCompilerHost(['/lib1/base.ts'], lib1Input, angularFiles);
|
|
const lib1AotHost = new MockAotCompilerHost(lib1Host);
|
|
lib1AotHost.tsFilesOnly();
|
|
const lib2Host = new MockCompilerHost(['/lib2/middle.ts'], lib2Input, angularFiles);
|
|
const lib2AotHost = new MockAotCompilerHost(lib2Host);
|
|
lib2AotHost.tsFilesOnly();
|
|
const appHost = new MockCompilerHost(['/app/main.ts'], appInput, angularFiles);
|
|
const appAotHost = new MockAotCompilerHost(appHost);
|
|
appAotHost.tsFilesOnly();
|
|
compile(lib1Host, lib1AotHost, expectNoDiagnostics, expectNoDiagnosticsAndEmit)
|
|
.then(() => {
|
|
lib1Host.writtenFiles.forEach((value, key) => lib2Host.writeFile(key, value, false));
|
|
lib1Host.overrides.forEach((value, key) => lib2Host.override(key, value));
|
|
return compile(
|
|
lib2Host, lib2AotHost, expectNoDiagnostics, expectNoDiagnosticsAndEmit);
|
|
})
|
|
.then(() => {
|
|
lib2Host.writtenFiles.forEach((value, key) => appHost.writeFile(key, value, false));
|
|
lib2Host.overrides.forEach((value, key) => appHost.override(key, value));
|
|
return compile(appHost, appAotHost, expectNoDiagnostics, expectNoDiagnosticsAndEmit);
|
|
})
|
|
.then((generatedFiles) => {
|
|
const mainNgFactory = generatedFiles.find(gf => gf.srcFileUrl === '/app/main.ts');
|
|
const flags = NodeFlags.TypeDirective | NodeFlags.Component | NodeFlags.OnDestroy;
|
|
expect(mainNgFactory.source)
|
|
.toContain(`${flags},(null as any),0,import1.Extends,[import2.AParam_2]`);
|
|
});
|
|
}));
|
|
|
|
describe('Injectable', () => {
|
|
it('should allow to inherit', async(() => {
|
|
compileParentAndChild({
|
|
parentClassDecorator: '@Injectable()',
|
|
parentModuleDecorator: '@NgModule({providers: [Base]})',
|
|
childClassDecorator: '@Injectable()',
|
|
childModuleDecorator: '@NgModule({providers: [Extends]})',
|
|
}).then((mainNgFactory) => { expect(mainNgFactory).toBeTruthy(); });
|
|
}));
|
|
|
|
it('should error if the child class has no matching decorator', async(() => {
|
|
compileParentAndChild({
|
|
parentClassDecorator: '@Injectable()',
|
|
parentModuleDecorator: '@NgModule({providers: [Base]})',
|
|
childClassDecorator: '',
|
|
childModuleDecorator: '@NgModule({providers: [Extends]})',
|
|
}).then(fail, (e) => {
|
|
expect(e.message).toContain(
|
|
'Class Extends in /app/main.ts extends from a Injectable in another compilation unit without duplicating the decorator. ' +
|
|
'Please add a Injectable or Pipe or Directive or Component or NgModule decorator to the class.');
|
|
});
|
|
}));
|
|
});
|
|
|
|
describe('Component', () => {
|
|
it('should allow to inherit', async(() => {
|
|
compileParentAndChild({
|
|
parentClassDecorator: `@Component({template: ''})`,
|
|
parentModuleDecorator: '@NgModule({declarations: [Base]})',
|
|
childClassDecorator: `@Component({template: ''})`,
|
|
childModuleDecorator: '@NgModule({declarations: [Extends]})',
|
|
}).then((mainNgFactory) => { expect(mainNgFactory).toBeTruthy(); });
|
|
}));
|
|
|
|
it('should error if the child class has no matching decorator', async(() => {
|
|
compileParentAndChild({
|
|
parentClassDecorator: `@Component({template: ''})`,
|
|
parentModuleDecorator: '@NgModule({declarations: [Base]})',
|
|
childClassDecorator: '',
|
|
childModuleDecorator: '@NgModule({declarations: [Extends]})',
|
|
}).then(fail, (e) => {
|
|
expect(e.message).toContain(
|
|
'Class Extends in /app/main.ts extends from a Directive in another compilation unit without duplicating the decorator. ' +
|
|
'Please add a Directive or Component decorator to the class.');
|
|
});
|
|
}));
|
|
});
|
|
|
|
describe('Directive', () => {
|
|
it('should allow to inherit', async(() => {
|
|
compileParentAndChild({
|
|
parentClassDecorator: `@Directive({selector: '[someDir]'})`,
|
|
parentModuleDecorator: '@NgModule({declarations: [Base]})',
|
|
childClassDecorator: `@Directive({selector: '[someDir]'})`,
|
|
childModuleDecorator: '@NgModule({declarations: [Extends]})',
|
|
}).then((mainNgFactory) => { expect(mainNgFactory).toBeTruthy(); });
|
|
}));
|
|
|
|
it('should error if the child class has no matching decorator', async(() => {
|
|
compileParentAndChild({
|
|
parentClassDecorator: `@Directive({selector: '[someDir]'})`,
|
|
parentModuleDecorator: '@NgModule({declarations: [Base]})',
|
|
childClassDecorator: '',
|
|
childModuleDecorator: '@NgModule({declarations: [Extends]})',
|
|
}).then(fail, (e) => {
|
|
expect(e.message).toContain(
|
|
'Class Extends in /app/main.ts extends from a Directive in another compilation unit without duplicating the decorator. ' +
|
|
'Please add a Directive or Component decorator to the class.');
|
|
});
|
|
}));
|
|
});
|
|
|
|
describe('Pipe', () => {
|
|
it('should allow to inherit', async(() => {
|
|
compileParentAndChild({
|
|
parentClassDecorator: `@Pipe({name: 'somePipe'})`,
|
|
parentModuleDecorator: '@NgModule({declarations: [Base]})',
|
|
childClassDecorator: `@Pipe({name: 'somePipe'})`,
|
|
childModuleDecorator: '@NgModule({declarations: [Extends]})',
|
|
}).then((mainNgFactory) => { expect(mainNgFactory).toBeTruthy(); });
|
|
}));
|
|
|
|
it('should error if the child class has no matching decorator', async(() => {
|
|
compileParentAndChild({
|
|
parentClassDecorator: `@Pipe({name: 'somePipe'})`,
|
|
parentModuleDecorator: '@NgModule({declarations: [Base]})',
|
|
childClassDecorator: '',
|
|
childModuleDecorator: '@NgModule({declarations: [Extends]})',
|
|
}).then(fail, (e) => {
|
|
expect(e.message).toContain(
|
|
'Class Extends in /app/main.ts extends from a Pipe in another compilation unit without duplicating the decorator. ' +
|
|
'Please add a Pipe decorator to the class.');
|
|
});
|
|
}));
|
|
});
|
|
|
|
describe('NgModule', () => {
|
|
it('should allow to inherit', async(() => {
|
|
compileParentAndChild({
|
|
parentClassDecorator: `@NgModule()`,
|
|
parentModuleDecorator: '',
|
|
childClassDecorator: `@NgModule()`,
|
|
childModuleDecorator: '',
|
|
}).then((mainNgFactory) => { expect(mainNgFactory).toBeTruthy(); });
|
|
}));
|
|
|
|
it('should error if the child class has no matching decorator', async(() => {
|
|
compileParentAndChild({
|
|
parentClassDecorator: `@NgModule()`,
|
|
parentModuleDecorator: '',
|
|
childClassDecorator: '',
|
|
childModuleDecorator: '',
|
|
}).then(fail, (e) => {
|
|
expect(e.message).toContain(
|
|
'Class Extends in /app/main.ts extends from a NgModule in another compilation unit without duplicating the decorator. ' +
|
|
'Please add a NgModule decorator to the class.');
|
|
});
|
|
}));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('compiler (bundled Angular)', () => {
|
|
let angularFiles: Map<string, string>;
|
|
|
|
beforeAll(() => {
|
|
const emittingHost = new EmittingCompilerHost(['@angular/core/index'], {emitMetadata: false});
|
|
|
|
// Create the metadata bundled
|
|
const indexModule = emittingHost.effectiveName('@angular/core/index');
|
|
const bundler = new MetadataBundler(
|
|
indexModule, '@angular/core', new MockMetadataBundlerHost(emittingHost));
|
|
const bundle = bundler.getMetadataBundle();
|
|
const metadata = JSON.stringify(bundle.metadata, null, ' ');
|
|
const bundleIndexSource = privateEntriesToIndex('./index', bundle.privates);
|
|
emittingHost.override('@angular/core/bundle_index.ts', bundleIndexSource);
|
|
emittingHost.addWrittenFile(
|
|
'@angular/core/package.json', JSON.stringify({typings: 'bundle_index.d.ts'}));
|
|
emittingHost.addWrittenFile('@angular/core/bundle_index.metadata.json', metadata);
|
|
|
|
// Emit the sources
|
|
const bundleIndexName = emittingHost.effectiveName('@angular/core/bundle_index.ts');
|
|
const emittingProgram = ts.createProgram([bundleIndexName], settings, emittingHost);
|
|
emittingProgram.emit();
|
|
angularFiles = emittingHost.written;
|
|
});
|
|
|
|
describe('Quickstart', () => {
|
|
let host: MockCompilerHost;
|
|
let aotHost: MockAotCompilerHost;
|
|
|
|
beforeEach(() => {
|
|
host = new MockCompilerHost(QUICKSTART, FILES, angularFiles);
|
|
aotHost = new MockAotCompilerHost(host);
|
|
});
|
|
|
|
// Restore reflector since AoT compiler will update it with a new static reflector
|
|
afterEach(() => { reflector.updateCapabilities(new ReflectionCapabilities()); });
|
|
|
|
it('should compile',
|
|
async(() => compile(host, aotHost, expectNoDiagnostics).then(generatedFiles => {
|
|
expect(generatedFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl)))
|
|
.toBeDefined();
|
|
expect(generatedFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl)))
|
|
.toBeDefined();
|
|
})));
|
|
});
|
|
|
|
describe('Bundled library', () => {
|
|
let host: MockCompilerHost;
|
|
let aotHost: MockAotCompilerHost;
|
|
let libraryFiles: Map<string, string>;
|
|
|
|
beforeAll(() => {
|
|
// Emit the library bundle
|
|
const emittingHost =
|
|
new EmittingCompilerHost(['/bolder/index.ts'], {emitMetadata: false, mockData: LIBRARY});
|
|
|
|
// Create the metadata bundled
|
|
const indexModule = '/bolder/public-api';
|
|
const bundler =
|
|
new MetadataBundler(indexModule, 'bolder', new MockMetadataBundlerHost(emittingHost));
|
|
const bundle = bundler.getMetadataBundle();
|
|
const metadata = JSON.stringify(bundle.metadata, null, ' ');
|
|
const bundleIndexSource = privateEntriesToIndex('./public-api', bundle.privates);
|
|
emittingHost.override('/bolder/index.ts', bundleIndexSource);
|
|
emittingHost.addWrittenFile('/bolder/index.metadata.json', metadata);
|
|
|
|
// Emit the sources
|
|
const emittingProgram = ts.createProgram(['/bolder/index.ts'], settings, emittingHost);
|
|
emittingProgram.emit();
|
|
libraryFiles = emittingHost.written;
|
|
|
|
// Copy the .html file
|
|
const htmlFileName = '/bolder/src/bolder.component.html';
|
|
libraryFiles.set(htmlFileName, emittingHost.readFile(htmlFileName));
|
|
});
|
|
|
|
beforeEach(() => {
|
|
host = new MockCompilerHost(
|
|
LIBRARY_USING_APP_MODULE, LIBRARY_USING_APP, angularFiles, [libraryFiles]);
|
|
aotHost = new MockAotCompilerHost(host);
|
|
});
|
|
|
|
it('should compile',
|
|
async(() => compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics)));
|
|
|
|
// Restore reflector since AoT compiler will update it with a new static reflector
|
|
afterEach(() => { reflector.updateCapabilities(new ReflectionCapabilities()); });
|
|
});
|
|
});
|
|
|
|
function expectNoDiagnostics(program: ts.Program) {
|
|
function fileInfo(diagnostic: ts.Diagnostic): string {
|
|
if (diagnostic.file) {
|
|
return `${diagnostic.file.fileName}(${diagnostic.start}): `;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function chars(len: number, ch: string): string { return new Array(len).fill(ch).join(''); }
|
|
|
|
function lineNoOf(offset: number, text: string): number {
|
|
let result = 1;
|
|
for (let i = 0; i < offset; i++) {
|
|
if (text[i] == '\n') result++;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function lineInfo(diagnostic: ts.Diagnostic): string {
|
|
if (diagnostic.file) {
|
|
const start = diagnostic.start;
|
|
let end = diagnostic.start + diagnostic.length;
|
|
const source = diagnostic.file.text;
|
|
let lineStart = start;
|
|
let lineEnd = end;
|
|
while (lineStart > 0 && source[lineStart] != '\n') lineStart--;
|
|
if (lineStart < start) lineStart++;
|
|
while (lineEnd < source.length && source[lineEnd] != '\n') lineEnd++;
|
|
let line = source.substring(lineStart, lineEnd);
|
|
const lineIndex = line.indexOf('/n');
|
|
if (lineIndex > 0) {
|
|
line = line.substr(0, lineIndex);
|
|
end = start + lineIndex;
|
|
}
|
|
const lineNo = lineNoOf(start, source) + ': ';
|
|
return '\n' + lineNo + line + '\n' + chars(start - lineStart + lineNo.length, ' ') +
|
|
chars(end - start, '^');
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) {
|
|
if (diagnostics && diagnostics.length) {
|
|
throw new Error(
|
|
'Errors from TypeScript:\n' +
|
|
diagnostics.map(d => `${fileInfo(d)}${d.messageText}${lineInfo(d)}`).join(' \n'));
|
|
}
|
|
}
|
|
expectNoDiagnostics(program.getOptionsDiagnostics());
|
|
expectNoDiagnostics(program.getSyntacticDiagnostics());
|
|
expectNoDiagnostics(program.getSemanticDiagnostics());
|
|
}
|
|
|
|
function expectNoDiagnosticsAndEmit(program: ts.Program) {
|
|
expectNoDiagnostics(program);
|
|
program.emit();
|
|
}
|
|
|
|
function isDTS(fileName: string): boolean {
|
|
return /\.d\.ts$/.test(fileName);
|
|
}
|
|
|
|
function isSource(fileName: string): boolean {
|
|
return /\.ts$/.test(fileName);
|
|
}
|
|
|
|
function isFactory(fileName: string): boolean {
|
|
return /\.ngfactory\./.test(fileName);
|
|
}
|
|
|
|
function summaryCompile(
|
|
host: MockCompilerHost, aotHost: MockAotCompilerHost,
|
|
preCompile?: (program: ts.Program) => void) {
|
|
// First compile the program to generate the summary files.
|
|
return compile(host, aotHost).then(generatedFiles => {
|
|
// Remove generated files that were not generated from a DTS file
|
|
host.remove(generatedFiles.filter(f => !isDTS(f.srcFileUrl)).map(f => f.genFileUrl));
|
|
|
|
// Next compile the program shrowding metadata and only treating .ts files as source.
|
|
aotHost.hideMetadata();
|
|
aotHost.tsFilesOnly();
|
|
|
|
return compile(host, aotHost);
|
|
});
|
|
}
|
|
|
|
function compile(
|
|
host: MockCompilerHost, aotHost: AotCompilerHost, preCompile?: (program: ts.Program) => void,
|
|
postCompile: (program: ts.Program) => void = expectNoDiagnostics,
|
|
options: AotCompilerOptions = {}): Promise<GeneratedFile[]> {
|
|
const scripts = host.scriptNames.slice(0);
|
|
const program = ts.createProgram(scripts, settings, host);
|
|
if (preCompile) preCompile(program);
|
|
const {compiler, reflector} = createAotCompiler(aotHost, options);
|
|
return compiler.compileAll(program.getSourceFiles().map(sf => sf.fileName))
|
|
.then(generatedFiles => {
|
|
generatedFiles.forEach(
|
|
file => isSource(file.genFileUrl) ? host.addScript(file.genFileUrl, file.source) :
|
|
host.override(file.genFileUrl, file.source));
|
|
const scripts = host.scriptNames.slice(0);
|
|
const newProgram = ts.createProgram(scripts, settings, host);
|
|
if (postCompile) postCompile(newProgram);
|
|
return generatedFiles;
|
|
});
|
|
}
|
|
|
|
const QUICKSTART = ['/quickstart/app/app.module.ts'];
|
|
const FILES: MockData = {
|
|
quickstart: {
|
|
app: {
|
|
'app.component.ts': `
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
template: '<h1>Hello {{name}}</h1>'
|
|
})
|
|
export class AppComponent {
|
|
name = 'Angular';
|
|
}
|
|
`,
|
|
'app.module.ts': `
|
|
import { NgModule } from '@angular/core';
|
|
import { toString } from './utils';
|
|
|
|
import { AppComponent } from './app.component';
|
|
|
|
@NgModule({
|
|
declarations: [ AppComponent ],
|
|
bootstrap: [ AppComponent ]
|
|
})
|
|
export class AppModule { }
|
|
`,
|
|
// #15420
|
|
'utils.ts': `
|
|
export function toString(value: any): string {
|
|
return '';
|
|
}
|
|
`
|
|
}
|
|
}
|
|
};
|
|
|
|
const LIBRARY: MockData = {
|
|
bolder: {
|
|
'public-api.ts': `
|
|
export * from './src/bolder.component';
|
|
export * from './src/bolder.module';
|
|
`,
|
|
src: {
|
|
'bolder.component.ts': `
|
|
import {Component, Input} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'bolder',
|
|
templateUrl: './bolder.component.html'
|
|
})
|
|
export class BolderComponent {
|
|
@Input() data: string;
|
|
}
|
|
`,
|
|
'bolder.component.html': `
|
|
<b>{{data}}</b>
|
|
`,
|
|
'bolder.module.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
import {BolderComponent} from './bolder.component';
|
|
|
|
@NgModule({
|
|
declarations: [BolderComponent],
|
|
exports: [BolderComponent]
|
|
})
|
|
export class BolderModule {}
|
|
`
|
|
}
|
|
}
|
|
};
|
|
|
|
const LIBRARY_USING_APP_MODULE = ['/lib-user/app/app.module.ts'];
|
|
const LIBRARY_USING_APP: MockData = {
|
|
'lib-user': {
|
|
app: {
|
|
'app.component.ts': `
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
template: '<h1>Hello <bolder [data]="name"></bolder></h1>'
|
|
})
|
|
export class AppComponent {
|
|
name = 'Angular';
|
|
}
|
|
`,
|
|
'app.module.ts': `
|
|
import { NgModule } from '@angular/core';
|
|
import { BolderModule } from 'bolder';
|
|
|
|
import { AppComponent } from './app.component';
|
|
|
|
@NgModule({
|
|
declarations: [ AppComponent ],
|
|
bootstrap: [ AppComponent ],
|
|
imports: [ BolderModule ]
|
|
})
|
|
export class AppModule { }
|
|
`
|
|
}
|
|
}
|
|
};
|
|
|
|
function expectPromiseToThrow(p: Promise<any>, msg: RegExp) {
|
|
p.then(
|
|
() => { throw new Error('Expected to throw'); }, (e) => { expect(e.message).toMatch(msg); });
|
|
}
|