refactor(compiler): simplify AOT tests

This commit is contained in:
Tobias Bosch 2017-04-26 09:24:42 -07:00 committed by Matias Niemelä
parent 21c96a5af1
commit c946a929b7
2 changed files with 308 additions and 351 deletions

View File

@ -6,8 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AotCompiler, AotCompilerHost, AotCompilerOptions, GeneratedFile, createAotCompiler} from '@angular/compiler'; import {GeneratedFile} from '@angular/compiler';
import {RenderComponentType, ɵReflectionCapabilities as ReflectionCapabilities, ɵreflector as reflector} from '@angular/core';
import {NodeFlags} from '@angular/core/src/view/index'; import {NodeFlags} from '@angular/core/src/view/index';
import {async} from '@angular/core/testing'; import {async} from '@angular/core/testing';
import {MetadataBundler, MetadataCollector, ModuleMetadata, privateEntriesToIndex} from '@angular/tsc-wrapped'; import {MetadataBundler, MetadataCollector, ModuleMetadata, privateEntriesToIndex} from '@angular/tsc-wrapped';
@ -15,60 +14,16 @@ import * as ts from 'typescript';
import {extractSourceMap, originalPositionFor} from '../output/source_map_util'; import {extractSourceMap, originalPositionFor} from '../output/source_map_util';
import {EmittingCompilerHost, MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, MockMetadataBundlerHost, settings} from './test_util'; import {EmittingCompilerHost, MockData, MockDirectory, MockMetadataBundlerHost, arrayToMockDir, arrayToMockMap, compile, settings, setup, toMockFileArray} 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)', () => { describe('compiler (unbundled Angular)', () => {
let angularFiles: Map<string, string>; let angularFiles = setup();
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', () => { describe('Quickstart', () => {
let host: MockCompilerHost; it('should compile', async(() => compile([QUICKSTART, angularFiles]).then(({genFiles}) => {
let aotHost: MockAotCompilerHost; expect(genFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl)))
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(); .toBeDefined();
expect(genFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl))).toBeDefined();
}))); })));
}); });
@ -97,17 +52,11 @@ describe('compiler (unbundled Angular)', () => {
}); });
function compileApp(): Promise<GeneratedFile> { function compileApp(): Promise<GeneratedFile> {
return new Promise((resolve, reject) => { return compile([rootDir, angularFiles])
const host = new MockCompilerHost(['/app/app.module.ts'], rootDir, angularFiles); .then(
const aotHost = new MockAotCompilerHost(host); ({genFiles}) => {return genFiles.find(
let result: GeneratedFile[]; genFile =>
let error: Error; genFile.srcFileUrl === componentPath && genFile.genFileUrl.endsWith('.ts'))});
resolve(compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics)
.then(
(files) => files.find(
genFile => genFile.srcFileUrl === componentPath &&
genFile.genFileUrl.endsWith('.ts'))));
});
} }
function findLineAndColumn( function findLineAndColumn(
@ -247,7 +196,7 @@ describe('compiler (unbundled Angular)', () => {
describe('errors', () => { describe('errors', () => {
it('should only warn if not all arguments of an @Injectable class can be resolved', it('should only warn if not all arguments of an @Injectable class can be resolved',
async(() => { async(() => {
const FILES: MockData = { const FILES: MockDirectory = {
app: { app: {
'app.ts': ` 'app.ts': `
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
@ -259,10 +208,8 @@ describe('compiler (unbundled Angular)', () => {
` `
} }
}; };
const host = new MockCompilerHost(['/app/app.ts'], FILES, angularFiles);
const aotHost = new MockAotCompilerHost(host);
const warnSpy = spyOn(console, 'warn'); const warnSpy = spyOn(console, 'warn');
compile(host, aotHost, expectNoDiagnostics).then(() => { compile([FILES, angularFiles]).then(() => {
expect(warnSpy).toHaveBeenCalledWith( expect(warnSpy).toHaveBeenCalledWith(
`Warning: Can't resolve all parameters for MyService in /app/app.ts: (?). This will become an error in Angular v5.x`); `Warning: Can't resolve all parameters for MyService in /app/app.ts: (?). This will become an error in Angular v5.x`);
}); });
@ -271,7 +218,7 @@ describe('compiler (unbundled Angular)', () => {
}); });
it('should add the preamble to generated files', async(() => { it('should add the preamble to generated files', async(() => {
const FILES: MockData = { const FILES: MockDirectory = {
app: { app: {
'app.ts': ` 'app.ts': `
import { NgModule, Component } from '@angular/core'; import { NgModule, Component } from '@angular/core';
@ -284,22 +231,19 @@ describe('compiler (unbundled Angular)', () => {
` `
} }
}; };
const host = new MockCompilerHost(['/app/app.ts'], FILES, angularFiles);
const aotHost = new MockAotCompilerHost(host);
const genFilePreamble = '/* Hello world! */'; const genFilePreamble = '/* Hello world! */';
compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics, {genFilePreamble}) compile([FILES, angularFiles], {genFilePreamble}).then(({genFiles}) => {
.then((generatedFiles) => { const genFile =
const genFile = generatedFiles.find( genFiles.find(gf => gf.srcFileUrl === '/app/app.ts' && gf.genFileUrl.endsWith('.ts'));
gf => gf.srcFileUrl === '/app/app.ts' && gf.genFileUrl.endsWith('.ts')); expect(genFile.source.startsWith(genFilePreamble)).toBe(true);
expect(genFile.source.startsWith(genFilePreamble)).toBe(true); });
});
})); }));
describe('ComponentFactories', () => { describe('ComponentFactories', () => {
it('should include inputs, outputs and ng-content selectors in the component factory', it('should include inputs, outputs and ng-content selectors in the component factory',
async(() => { async(() => {
const FILES: MockData = { const FILES: MockDirectory = {
app: { app: {
'app.ts': ` 'app.ts': `
import {Component, NgModule, Input, Output} from '@angular/core'; import {Component, NgModule, Input, Output} from '@angular/core';
@ -323,11 +267,8 @@ describe('compiler (unbundled Angular)', () => {
` `
} }
}; };
const host = new MockCompilerHost(['/app/app.ts'], FILES, angularFiles); compile([FILES, angularFiles]).then(({genFiles}) => {
const aotHost = new MockAotCompilerHost(host); const genFile = genFiles.find(genFile => genFile.srcFileUrl === '/app/app.ts');
let generatedFiles: GeneratedFile[];
compile(host, aotHost, expectNoDiagnostics).then((generatedFiles) => {
const genFile = generatedFiles.find(genFile => genFile.srcFileUrl === '/app/app.ts');
const createComponentFactoryCall = const createComponentFactoryCall =
/ɵccf\([^)]*\)/m.exec(genFile.source) ![0].replace(/\s*/g, ''); /ɵccf\([^)]*\)/m.exec(genFile.source) ![0].replace(/\s*/g, '');
// selector // selector
@ -345,7 +286,7 @@ describe('compiler (unbundled Angular)', () => {
describe('generated templates', () => { describe('generated templates', () => {
it('should not call `check` for directives without bindings nor ngDoCheck/ngOnInit', it('should not call `check` for directives without bindings nor ngDoCheck/ngOnInit',
async(() => { async(() => {
const FILES: MockData = { const FILES: MockDirectory = {
app: { app: {
'app.ts': ` 'app.ts': `
import { NgModule, Component } from '@angular/core'; import { NgModule, Component } from '@angular/core';
@ -358,37 +299,16 @@ describe('compiler (unbundled Angular)', () => {
` `
} }
}; };
const host = new MockCompilerHost(['/app/app.ts'], FILES, angularFiles); compile([FILES, angularFiles]).then(({genFiles}) => {
const aotHost = new MockAotCompilerHost(host); const genFile = genFiles.find(
const genFilePreamble = '/* Hello world! */'; gf => gf.srcFileUrl === '/app/app.ts' && gf.genFileUrl.endsWith('.ts'));
compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics, {genFilePreamble}) expect(genFile.source).not.toContain('check(');
.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', () => { 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( function compileParentAndChild(
{parentClassDecorator, parentModuleDecorator, childClassDecorator, childModuleDecorator}: { {parentClassDecorator, parentModuleDecorator, childClassDecorator, childModuleDecorator}: {
parentClassDecorator: string, parentClassDecorator: string,
@ -396,7 +316,7 @@ describe('compiler (unbundled Angular)', () => {
childClassDecorator: string, childClassDecorator: string,
childModuleDecorator: string childModuleDecorator: string
}) { }) {
const libInput: MockData = { const libInput: MockDirectory = {
'lib': { 'lib': {
'base.ts': ` 'base.ts': `
import {Injectable, Pipe, Directive, Component, NgModule} from '@angular/core'; import {Injectable, Pipe, Directive, Component, NgModule} from '@angular/core';
@ -409,7 +329,7 @@ describe('compiler (unbundled Angular)', () => {
` `
} }
}; };
const appInput: MockData = { const appInput: MockDirectory = {
'app': { 'app': {
'main.ts': ` 'main.ts': `
import {Injectable, Pipe, Directive, Component, NgModule} from '@angular/core'; import {Injectable, Pipe, Directive, Component, NgModule} from '@angular/core';
@ -424,13 +344,14 @@ describe('compiler (unbundled Angular)', () => {
} }
}; };
return compileWithSummaries(libInput, appInput) return compile([libInput, angularFiles], {useSummaries: true})
.then((generatedFiles) => generatedFiles.find(gf => gf.srcFileUrl === '/app/main.ts')); .then(({outDir}) => compile([outDir, appInput, angularFiles], {useSummaries: true}))
.then(({genFiles}) => genFiles.find(gf => gf.srcFileUrl === '/app/main.ts'));
} }
it('should inherit ctor and lifecycle hooks from classes in other compilation units', it('should inherit ctor and lifecycle hooks from classes in other compilation units',
async(() => { async(() => {
const libInput: MockData = { const libInput: MockDirectory = {
'lib': { 'lib': {
'base.ts': ` 'base.ts': `
export class AParam {} export class AParam {}
@ -442,7 +363,7 @@ describe('compiler (unbundled Angular)', () => {
` `
} }
}; };
const appInput: MockData = { const appInput: MockDirectory = {
'app': { 'app': {
'main.ts': ` 'main.ts': `
import {NgModule, Component} from '@angular/core'; import {NgModule, Component} from '@angular/core';
@ -459,17 +380,19 @@ describe('compiler (unbundled Angular)', () => {
} }
}; };
compileWithSummaries(libInput, appInput).then((generatedFiles) => { compile([libInput, angularFiles], {useSummaries: true})
const mainNgFactory = generatedFiles.find(gf => gf.srcFileUrl === '/app/main.ts'); .then(({outDir}) => compile([outDir, appInput, angularFiles], {useSummaries: true}))
const flags = NodeFlags.TypeDirective | NodeFlags.Component | NodeFlags.OnDestroy; .then(({genFiles}) => {
expect(mainNgFactory.source) const mainNgFactory = genFiles.find(gf => gf.srcFileUrl === '/app/main.ts');
.toContain(`${flags},(null as any),0,import1.Extends,[import2.AParam]`); 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', it('should inherit ctor and lifecycle hooks from classes in other compilation units over 2 levels',
async(() => { async(() => {
const lib1Input: MockData = { const lib1Input: MockDirectory = {
'lib1': { 'lib1': {
'base.ts': ` 'base.ts': `
export class AParam {} export class AParam {}
@ -482,7 +405,7 @@ describe('compiler (unbundled Angular)', () => {
} }
}; };
const lib2Input: MockData = { const lib2Input: MockDirectory = {
'lib2': { 'lib2': {
'middle.ts': ` 'middle.ts': `
import {Base} from '../lib1/base'; import {Base} from '../lib1/base';
@ -492,7 +415,7 @@ describe('compiler (unbundled Angular)', () => {
}; };
const appInput: MockData = { const appInput: MockDirectory = {
'app': { 'app': {
'main.ts': ` 'main.ts': `
import {NgModule, Component} from '@angular/core'; import {NgModule, Component} from '@angular/core';
@ -508,29 +431,11 @@ describe('compiler (unbundled Angular)', () => {
` `
} }
}; };
const lib1Host = new MockCompilerHost(['/lib1/base.ts'], lib1Input, angularFiles); compile([lib1Input, angularFiles], {useSummaries: true})
const lib1AotHost = new MockAotCompilerHost(lib1Host); .then(({outDir}) => compile([outDir, lib2Input, angularFiles], {useSummaries: true}))
lib1AotHost.tsFilesOnly(); .then(({outDir}) => compile([outDir, appInput, angularFiles], {useSummaries: true}))
const lib2Host = new MockCompilerHost(['/lib2/middle.ts'], lib2Input, angularFiles); .then(({genFiles}) => {
const lib2AotHost = new MockAotCompilerHost(lib2Host); const mainNgFactory = genFiles.find(gf => gf.srcFileUrl === '/app/main.ts');
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; const flags = NodeFlags.TypeDirective | NodeFlags.Component | NodeFlags.OnDestroy;
expect(mainNgFactory.source) expect(mainNgFactory.source)
.toContain(`${flags},(null as any),0,import1.Extends,[import2.AParam_2]`); .toContain(`${flags},(null as any),0,import1.Extends,[import2.AParam_2]`);
@ -660,6 +565,8 @@ describe('compiler (unbundled Angular)', () => {
}); });
describe('compiler (bundled Angular)', () => { describe('compiler (bundled Angular)', () => {
setup({compileAngular: false});
let angularFiles: Map<string, string>; let angularFiles: Map<string, string>;
beforeAll(() => { beforeAll(() => {
@ -681,34 +588,19 @@ describe('compiler (bundled Angular)', () => {
const bundleIndexName = emittingHost.effectiveName('@angular/core/bundle_index.ts'); const bundleIndexName = emittingHost.effectiveName('@angular/core/bundle_index.ts');
const emittingProgram = ts.createProgram([bundleIndexName], settings, emittingHost); const emittingProgram = ts.createProgram([bundleIndexName], settings, emittingHost);
emittingProgram.emit(); emittingProgram.emit();
angularFiles = emittingHost.written; angularFiles = emittingHost.writtenAngularFiles();
}); });
describe('Quickstart', () => { describe('Quickstart', () => {
let host: MockCompilerHost; it('should compile', async(() => compile([QUICKSTART, angularFiles]).then(({genFiles}) => {
let aotHost: MockAotCompilerHost; expect(genFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl)))
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(); .toBeDefined();
expect(genFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl))).toBeDefined();
}))); })));
}); });
describe('Bundled library', () => { describe('Bundled library', () => {
let host: MockCompilerHost; let libraryFiles: MockDirectory;
let aotHost: MockAotCompilerHost;
let libraryFiles: Map<string, string>;
beforeAll(() => { beforeAll(() => {
// Emit the library bundle // Emit the library bundle
@ -728,135 +620,22 @@ describe('compiler (bundled Angular)', () => {
// Emit the sources // Emit the sources
const emittingProgram = ts.createProgram(['/bolder/index.ts'], settings, emittingHost); const emittingProgram = ts.createProgram(['/bolder/index.ts'], settings, emittingHost);
emittingProgram.emit(); emittingProgram.emit();
libraryFiles = emittingHost.written; const libFiles = emittingHost.written;
// Copy the .html file // Copy the .html file
const htmlFileName = '/bolder/src/bolder.component.html'; const htmlFileName = '/bolder/src/bolder.component.html';
libraryFiles.set(htmlFileName, emittingHost.readFile(htmlFileName)); libFiles.set(htmlFileName, emittingHost.readFile(htmlFileName));
libraryFiles = arrayToMockDir(toMockFileArray(libFiles).map(
({fileName, content}) => ({fileName: `/node_modules${fileName}`, content})));
}); });
beforeEach(() => { it('should compile', async(() => compile([LIBRARY_USING_APP, libraryFiles, angularFiles])));
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(''); } const QUICKSTART: MockDirectory = {
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: { quickstart: {
app: { app: {
'app.component.ts': ` 'app.component.ts': `
@ -891,7 +670,7 @@ const FILES: MockData = {
} }
}; };
const LIBRARY: MockData = { const LIBRARY: MockDirectory = {
bolder: { bolder: {
'public-api.ts': ` 'public-api.ts': `
export * from './src/bolder.component'; export * from './src/bolder.component';
@ -927,7 +706,7 @@ const LIBRARY: MockData = {
}; };
const LIBRARY_USING_APP_MODULE = ['/lib-user/app/app.module.ts']; const LIBRARY_USING_APP_MODULE = ['/lib-user/app/app.module.ts'];
const LIBRARY_USING_APP: MockData = { const LIBRARY_USING_APP: MockDirectory = {
'lib-user': { 'lib-user': {
app: { app: {
'app.component.ts': ` 'app.component.ts': `

View File

@ -6,19 +6,26 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AotCompilerHost} from '@angular/compiler'; import {AotCompilerHost, AotCompilerOptions, GeneratedFile, createAotCompiler} from '@angular/compiler';
import {ɵReflectionCapabilities as ReflectionCapabilities, ɵreflector as reflector} from '@angular/core';
import {MetadataBundlerHost, MetadataCollector, ModuleMetadata} from '@angular/tsc-wrapped'; import {MetadataBundlerHost, MetadataCollector, ModuleMetadata} from '@angular/tsc-wrapped';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as ts from 'typescript'; import * as ts from 'typescript';
export type MockData = string | MockDirectory; let nodeModulesPath: string;
let angularSourcePath: string;
let rootPath: string;
calcPathsOnDisc();
export type MockFileOrDirectory = string | MockDirectory;
export type MockDirectory = { export type MockDirectory = {
[name: string]: MockData | undefined; [name: string]: MockFileOrDirectory | undefined;
}; };
export function isDirectory(data: MockData | undefined): data is MockDirectory { export function isDirectory(data: MockFileOrDirectory | undefined): data is MockDirectory {
return typeof data !== 'string'; return typeof data !== 'string';
} }
@ -43,12 +50,21 @@ export const settings: ts.CompilerOptions = {
export interface EmitterOptions { export interface EmitterOptions {
emitMetadata: boolean; emitMetadata: boolean;
mockData?: MockData; mockData?: MockDirectory;
} }
function calcPathsOnDisc() {
const moduleFilename = module.filename.replace(/\\/g, '/');
const distIndex = moduleFilename.indexOf('/dist/all');
if (distIndex >= 0) {
rootPath = moduleFilename.substr(0, distIndex);
nodeModulesPath = path.join(rootPath, 'node_modules');
angularSourcePath = path.join(rootPath, 'packages');
}
}
export class EmittingCompilerHost implements ts.CompilerHost { export class EmittingCompilerHost implements ts.CompilerHost {
private angularSourcePath: string|undefined;
private nodeModulesPath: string|undefined;
private addedFiles = new Map<string, string>(); private addedFiles = new Map<string, string>();
private writtenFiles = new Map<string, string>(); private writtenFiles = new Map<string, string>();
private scriptNames: string[]; private scriptNames: string[];
@ -56,19 +72,18 @@ export class EmittingCompilerHost implements ts.CompilerHost {
private collector = new MetadataCollector(); private collector = new MetadataCollector();
constructor(scriptNames: string[], private options: EmitterOptions) { constructor(scriptNames: string[], private options: EmitterOptions) {
const moduleFilename = module.filename.replace(/\\/g, '/'); // Rewrite references to scripts with '@angular' to its corresponding location in
const distIndex = moduleFilename.indexOf('/dist/all'); // the source tree.
if (distIndex >= 0) { this.scriptNames = scriptNames.map(f => this.effectiveName(f));
const root = moduleFilename.substr(0, distIndex); this.root = rootPath;
this.nodeModulesPath = path.join(root, 'node_modules'); }
this.angularSourcePath = path.join(root, 'packages');
// Rewrite references to scripts with '@angular' to its corresponding location in public writtenAngularFiles(target = new Map<string, string>()): Map<string, string> {
// the source tree. this.written.forEach((value, key) => {
this.scriptNames = scriptNames.map(f => this.effectiveName(f)); const path = `/node_modules/@angular${key.substring(angularSourcePath.length)}`;
target.set(path, value);
this.root = root; });
} return target;
} }
public addScript(fileName: string, content: string) { public addScript(fileName: string, content: string) {
@ -97,7 +112,7 @@ export class EmittingCompilerHost implements ts.CompilerHost {
public effectiveName(fileName: string): string { public effectiveName(fileName: string): string {
const prefix = '@angular/'; const prefix = '@angular/';
return fileName.startsWith('@angular/') ? return fileName.startsWith('@angular/') ?
path.join(this.angularSourcePath, fileName.substr(prefix.length)) : path.join(angularSourcePath, fileName.substr(prefix.length)) :
fileName; fileName;
} }
@ -171,31 +186,17 @@ export class EmittingCompilerHost implements ts.CompilerHost {
getNewLine(): string { return '\n'; } getNewLine(): string { return '\n'; }
} }
const MOCK_NODEMODULES_PREFIX = '/node_modules/';
export class MockCompilerHost implements ts.CompilerHost { export class MockCompilerHost implements ts.CompilerHost {
scriptNames: string[]; scriptNames: string[];
private angularSourcePath: string|undefined;
private nodeModulesPath: string|undefined;
public overrides = new Map<string, string>(); public overrides = new Map<string, string>();
public writtenFiles = new Map<string, string>(); public writtenFiles = new Map<string, string>();
private sourceFiles = new Map<string, ts.SourceFile>(); private sourceFiles = new Map<string, ts.SourceFile>();
private assumeExists = new Set<string>(); private assumeExists = new Set<string>();
private traces: string[] = []; private traces: string[] = [];
constructor( constructor(scriptNames: string[], private data: MockDirectory) {
scriptNames: string[], private data: MockData, private angular: Map<string, string>,
private libraries?: Map<string, string>[]) {
this.scriptNames = scriptNames.slice(0); this.scriptNames = scriptNames.slice(0);
const moduleFilename = module.filename.replace(/\\/g, '/');
let angularIndex = moduleFilename.indexOf('@angular');
let distIndex = moduleFilename.indexOf('/dist/all');
if (distIndex >= 0) {
const root = moduleFilename.substr(0, distIndex);
this.nodeModulesPath = path.join(root, 'node_modules');
this.angularSourcePath = path.join(root, 'packages');
}
} }
// Test API // Test API
@ -234,22 +235,13 @@ export class MockCompilerHost implements ts.CompilerHost {
const effectiveName = this.getEffectiveName(fileName); const effectiveName = this.getEffectiveName(fileName);
if (effectiveName == fileName) { if (effectiveName == fileName) {
let result = open(fileName, this.data) != null; let result = open(fileName, this.data) != null;
if (!result && fileName.startsWith(MOCK_NODEMODULES_PREFIX)) {
const libraryPath = fileName.substr(MOCK_NODEMODULES_PREFIX.length - 1);
for (const library of this.libraries !) {
if (library.has(libraryPath)) {
return true;
}
}
}
return result; return result;
} else { } else {
if (fileName.match(rxjs)) { if (fileName.match(rxjs)) {
let result = fs.existsSync(effectiveName); let result = fs.existsSync(effectiveName);
return result; return result;
} }
const result = this.angular.has(effectiveName); return false;
return result;
} }
} }
@ -315,12 +307,6 @@ export class MockCompilerHost implements ts.CompilerHost {
let effectiveName = this.getEffectiveName(fileName); let effectiveName = this.getEffectiveName(fileName);
if (effectiveName === fileName) { if (effectiveName === fileName) {
const result = open(fileName, this.data); const result = open(fileName, this.data);
if (!result && fileName.startsWith(MOCK_NODEMODULES_PREFIX)) {
const libraryPath = fileName.substr(MOCK_NODEMODULES_PREFIX.length - 1);
for (const library of this.libraries !) {
if (library.has(libraryPath)) return library.get(libraryPath);
}
}
return result; return result;
} else { } else {
if (fileName.match(rxjs)) { if (fileName.match(rxjs)) {
@ -328,22 +314,16 @@ export class MockCompilerHost implements ts.CompilerHost {
return fs.readFileSync(fileName, 'utf8'); return fs.readFileSync(fileName, 'utf8');
} }
} }
return this.angular.get(effectiveName);
} }
} }
} }
private getEffectiveName(name: string): string { private getEffectiveName(name: string): string {
const node_modules = 'node_modules'; const node_modules = 'node_modules';
const at_angular = '/@angular';
const rxjs = '/rxjs'; const rxjs = '/rxjs';
if (name.startsWith('/' + node_modules)) { if (name.startsWith('/' + node_modules)) {
if (this.angularSourcePath && name.startsWith('/' + node_modules + at_angular)) { if (nodeModulesPath && name.startsWith('/' + node_modules + rxjs)) {
return path.join( return path.join(nodeModulesPath, name.substr(node_modules.length + 1));
this.angularSourcePath, name.substr(node_modules.length + at_angular.length + 1));
}
if (this.nodeModulesPath && name.startsWith('/' + node_modules + rxjs)) {
return path.join(this.nodeModulesPath, name.substr(node_modules.length + 1));
} }
} }
return name; return name;
@ -439,11 +419,12 @@ export class MockMetadataBundlerHost implements MetadataBundlerHost {
} }
} }
function find(fileName: string, data: MockData | undefined): MockData|undefined { function find(fileName: string, data: MockFileOrDirectory | undefined): MockFileOrDirectory|
undefined {
if (!data) return undefined; if (!data) return undefined;
let names = fileName.split('/'); let names = fileName.split('/');
if (names.length && !names[0].length) names.shift(); if (names.length && !names[0].length) names.shift();
let current: MockData|undefined = data; let current: MockFileOrDirectory|undefined = data;
for (let name of names) { for (let name of names) {
if (typeof current === 'string') if (typeof current === 'string')
return undefined; return undefined;
@ -454,7 +435,7 @@ function find(fileName: string, data: MockData | undefined): MockData|undefined
return current; return current;
} }
function open(fileName: string, data: MockData | undefined): string|undefined { function open(fileName: string, data: MockFileOrDirectory | undefined): string|undefined {
let result = find(fileName, data); let result = find(fileName, data);
if (typeof result === 'string') { if (typeof result === 'string') {
return result; return result;
@ -462,7 +443,204 @@ function open(fileName: string, data: MockData | undefined): string|undefined {
return undefined; return undefined;
} }
function directoryExists(dirname: string, data: MockData | undefined): boolean { function directoryExists(dirname: string, data: MockFileOrDirectory | undefined): boolean {
let result = find(dirname, data); let result = find(dirname, data);
return !!result && typeof result !== 'string'; return !!result && typeof result !== 'string';
} }
export type MockFileArray = {
fileName: string,
content: string
}[];
export type MockData = MockDirectory | Map<string, string>| (MockDirectory | Map<string, string>)[];
export function toMockFileArray(data: MockData, target: MockFileArray = []): MockFileArray {
if (data instanceof Map) {
mapToMockFileArray(data, target);
} else if (Array.isArray(data)) {
data.forEach(entry => toMockFileArray(entry, target));
} else {
mockDirToFileArray(data, '', target);
}
return target;
}
function mockDirToFileArray(dir: MockDirectory, path: string, target: MockFileArray) {
Object.keys(dir).forEach((localFileName) => {
const value = dir[localFileName] !;
const fileName = `${path}/${localFileName}`;
if (typeof value === 'string') {
target.push({fileName, content: value});
} else {
mockDirToFileArray(value, fileName, target);
}
});
}
function mapToMockFileArray(files: Map<string, string>, target: MockFileArray) {
files.forEach((content, fileName) => { target.push({fileName, content}); });
}
export function arrayToMockMap(arr: MockFileArray): Map<string, string> {
const map = new Map<string, string>();
arr.forEach(({fileName, content}) => { map.set(fileName, content); });
return map;
}
export function arrayToMockDir(arr: MockFileArray): MockDirectory {
const rootDir: MockDirectory = {};
arr.forEach(({fileName, content}) => {
let pathParts = fileName.split('/');
// trim trailing slash
let startIndex = pathParts[0] ? 0 : 1;
// get/create the directory
let currentDir = rootDir;
for (let i = startIndex; i < pathParts.length - 1; i++) {
const pathPart = pathParts[i];
let localDir = <MockDirectory>currentDir[pathPart];
if (!localDir) {
currentDir[pathPart] = localDir = {};
}
currentDir = localDir;
}
// write the file
currentDir[pathParts[pathParts.length - 1]] = content;
});
return rootDir;
}
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';
`;
export function setup(options: {compileAngular: boolean} = {
compileAngular: true
}) {
let angularFiles = new Map<string, string>();
beforeAll(() => {
if (options.compileAngular) {
const emittingHost = new EmittingCompilerHost([], {emitMetadata: true});
emittingHost.addScript('@angular/core/index.ts', minCoreIndex);
const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost);
emittingProgram.emit();
emittingHost.writtenAngularFiles(angularFiles);
}
});
// Restore reflector since AoT compiler will update it with a new static reflector
afterEach(() => { reflector.updateCapabilities(new ReflectionCapabilities()); });
return angularFiles;
}
export 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 isSource(fileName: string): boolean {
return !/\.d\.ts$/.test(fileName) && /\.ts$/.test(fileName);
}
export function compile(rootDirs: MockData, options: {
emit?: boolean,
useSummaries?: boolean,
preCompile?: (program: ts.Program) => void,
postCompile?: (program: ts.Program) => void,
}& AotCompilerOptions = {}): Promise<{genFiles: GeneratedFile[], outDir: MockDirectory}> {
// Make sure we always return errors via the promise...
return Promise.resolve(null).then(() => {
// when using summaries, always emit so the next step can use the results.
const emit = options.emit || options.useSummaries;
const preCompile = options.preCompile || expectNoDiagnostics;
const postCompile = options.postCompile || expectNoDiagnostics;
const rootDirArr = toMockFileArray(rootDirs);
const scriptNames = rootDirArr.map(entry => entry.fileName).filter(isSource);
const host = new MockCompilerHost(scriptNames, arrayToMockDir(rootDirArr));
const aotHost = new MockAotCompilerHost(host);
if (options.useSummaries) {
aotHost.hideMetadata();
aotHost.tsFilesOnly();
}
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(genFiles => {
genFiles.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);
if (emit) {
newProgram.emit();
}
let outDir: MockDirectory = {};
if (emit) {
outDir = arrayToMockDir(toMockFileArray([
host.writtenFiles, host.overrides
]).filter((entry) => !isSource(entry.fileName)));
}
return {genFiles, outDir};
});
});
}