From c946a929b7d58e7694787a2dab3cf79a4fd9ab46 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Wed, 26 Apr 2017 09:24:42 -0700 Subject: [PATCH] refactor(compiler): simplify AOT tests --- packages/compiler/test/aot/compiler_spec.ts | 355 ++++---------------- packages/compiler/test/aot/test_util.ts | 304 +++++++++++++---- 2 files changed, 308 insertions(+), 351 deletions(-) diff --git a/packages/compiler/test/aot/compiler_spec.ts b/packages/compiler/test/aot/compiler_spec.ts index 77e3a0a8a3..81752f5ac5 100644 --- a/packages/compiler/test/aot/compiler_spec.ts +++ b/packages/compiler/test/aot/compiler_spec.ts @@ -6,8 +6,7 @@ * 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 {GeneratedFile} from '@angular/compiler'; import {NodeFlags} from '@angular/core/src/view/index'; import {async} from '@angular/core/testing'; 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 {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'; -`; +import {EmittingCompilerHost, MockData, MockDirectory, MockMetadataBundlerHost, arrayToMockDir, arrayToMockMap, compile, settings, setup, toMockFileArray} from './test_util'; describe('compiler (unbundled Angular)', () => { - let angularFiles: Map; - - 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()); }); + let angularFiles = setup(); 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))) + it('should compile', async(() => compile([QUICKSTART, angularFiles]).then(({genFiles}) => { + expect(genFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl))) .toBeDefined(); + expect(genFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl))).toBeDefined(); }))); }); @@ -97,17 +52,11 @@ describe('compiler (unbundled Angular)', () => { }); function compileApp(): Promise { - 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')))); - }); + return compile([rootDir, angularFiles]) + .then( + ({genFiles}) => {return genFiles.find( + genFile => + genFile.srcFileUrl === componentPath && genFile.genFileUrl.endsWith('.ts'))}); } function findLineAndColumn( @@ -247,7 +196,7 @@ describe('compiler (unbundled Angular)', () => { describe('errors', () => { it('should only warn if not all arguments of an @Injectable class can be resolved', async(() => { - const FILES: MockData = { + const FILES: MockDirectory = { app: { 'app.ts': ` 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'); - compile(host, aotHost, expectNoDiagnostics).then(() => { + compile([FILES, angularFiles]).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`); }); @@ -271,7 +218,7 @@ describe('compiler (unbundled Angular)', () => { }); it('should add the preamble to generated files', async(() => { - const FILES: MockData = { + const FILES: MockDirectory = { app: { 'app.ts': ` 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! */'; - 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); - }); + compile([FILES, angularFiles], {genFilePreamble}).then(({genFiles}) => { + const genFile = + genFiles.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 = { + const FILES: MockDirectory = { app: { 'app.ts': ` 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); - const aotHost = new MockAotCompilerHost(host); - let generatedFiles: GeneratedFile[]; - compile(host, aotHost, expectNoDiagnostics).then((generatedFiles) => { - const genFile = generatedFiles.find(genFile => genFile.srcFileUrl === '/app/app.ts'); + compile([FILES, angularFiles]).then(({genFiles}) => { + const genFile = genFiles.find(genFile => genFile.srcFileUrl === '/app/app.ts'); const createComponentFactoryCall = /ɵccf\([^)]*\)/m.exec(genFile.source) ![0].replace(/\s*/g, ''); // selector @@ -345,7 +286,7 @@ describe('compiler (unbundled Angular)', () => { describe('generated templates', () => { it('should not call `check` for directives without bindings nor ngDoCheck/ngOnInit', async(() => { - const FILES: MockData = { + const FILES: MockDirectory = { app: { 'app.ts': ` import { NgModule, Component } from '@angular/core'; @@ -358,37 +299,16 @@ describe('compiler (unbundled Angular)', () => { ` } }; - 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('); - }); + compile([FILES, angularFiles]).then(({genFiles}) => { + const genFile = genFiles.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 { - 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, @@ -396,7 +316,7 @@ describe('compiler (unbundled Angular)', () => { childClassDecorator: string, childModuleDecorator: string }) { - const libInput: MockData = { + const libInput: MockDirectory = { 'lib': { 'base.ts': ` import {Injectable, Pipe, Directive, Component, NgModule} from '@angular/core'; @@ -409,7 +329,7 @@ describe('compiler (unbundled Angular)', () => { ` } }; - const appInput: MockData = { + const appInput: MockDirectory = { 'app': { 'main.ts': ` import {Injectable, Pipe, Directive, Component, NgModule} from '@angular/core'; @@ -424,13 +344,14 @@ describe('compiler (unbundled Angular)', () => { } }; - return compileWithSummaries(libInput, appInput) - .then((generatedFiles) => generatedFiles.find(gf => gf.srcFileUrl === '/app/main.ts')); + return compile([libInput, angularFiles], {useSummaries: true}) + .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', async(() => { - const libInput: MockData = { + const libInput: MockDirectory = { 'lib': { 'base.ts': ` export class AParam {} @@ -442,7 +363,7 @@ describe('compiler (unbundled Angular)', () => { ` } }; - const appInput: MockData = { + const appInput: MockDirectory = { 'app': { 'main.ts': ` import {NgModule, Component} from '@angular/core'; @@ -459,17 +380,19 @@ describe('compiler (unbundled Angular)', () => { } }; - 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]`); - }); + compile([libInput, angularFiles], {useSummaries: true}) + .then(({outDir}) => compile([outDir, appInput, angularFiles], {useSummaries: true})) + .then(({genFiles}) => { + const mainNgFactory = genFiles.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 = { + const lib1Input: MockDirectory = { 'lib1': { 'base.ts': ` export class AParam {} @@ -482,7 +405,7 @@ describe('compiler (unbundled Angular)', () => { } }; - const lib2Input: MockData = { + const lib2Input: MockDirectory = { 'lib2': { 'middle.ts': ` import {Base} from '../lib1/base'; @@ -492,7 +415,7 @@ describe('compiler (unbundled Angular)', () => { }; - const appInput: MockData = { + const appInput: MockDirectory = { 'app': { 'main.ts': ` import {NgModule, Component} from '@angular/core'; @@ -508,29 +431,11 @@ describe('compiler (unbundled Angular)', () => { ` } }; - 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'); + compile([lib1Input, angularFiles], {useSummaries: true}) + .then(({outDir}) => compile([outDir, lib2Input, angularFiles], {useSummaries: true})) + .then(({outDir}) => compile([outDir, appInput, angularFiles], {useSummaries: true})) + .then(({genFiles}) => { + const mainNgFactory = genFiles.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]`); @@ -660,6 +565,8 @@ describe('compiler (unbundled Angular)', () => { }); describe('compiler (bundled Angular)', () => { + setup({compileAngular: false}); + let angularFiles: Map; beforeAll(() => { @@ -681,34 +588,19 @@ describe('compiler (bundled Angular)', () => { const bundleIndexName = emittingHost.effectiveName('@angular/core/bundle_index.ts'); const emittingProgram = ts.createProgram([bundleIndexName], settings, emittingHost); emittingProgram.emit(); - angularFiles = emittingHost.written; + angularFiles = emittingHost.writtenAngularFiles(); }); 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))) + it('should compile', async(() => compile([QUICKSTART, angularFiles]).then(({genFiles}) => { + expect(genFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl))) .toBeDefined(); + expect(genFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl))).toBeDefined(); }))); }); describe('Bundled library', () => { - let host: MockCompilerHost; - let aotHost: MockAotCompilerHost; - let libraryFiles: Map; + let libraryFiles: MockDirectory; beforeAll(() => { // Emit the library bundle @@ -728,135 +620,22 @@ describe('compiler (bundled Angular)', () => { // Emit the sources const emittingProgram = ts.createProgram(['/bolder/index.ts'], settings, emittingHost); emittingProgram.emit(); - libraryFiles = emittingHost.written; + const libFiles = emittingHost.written; // Copy the .html file 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(() => { - 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()); }); + it('should compile', async(() => compile([LIBRARY_USING_APP, libraryFiles, angularFiles]))); }); }); -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 { - 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 = { +const QUICKSTART: MockDirectory = { quickstart: { app: { 'app.component.ts': ` @@ -891,7 +670,7 @@ const FILES: MockData = { } }; -const LIBRARY: MockData = { +const LIBRARY: MockDirectory = { bolder: { 'public-api.ts': ` 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: MockData = { +const LIBRARY_USING_APP: MockDirectory = { 'lib-user': { app: { 'app.component.ts': ` diff --git a/packages/compiler/test/aot/test_util.ts b/packages/compiler/test/aot/test_util.ts index 1b81fb8319..c76ad271c3 100644 --- a/packages/compiler/test/aot/test_util.ts +++ b/packages/compiler/test/aot/test_util.ts @@ -6,19 +6,26 @@ * 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 * as fs from 'fs'; import * as path from 'path'; 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 = { - [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'; } @@ -43,12 +50,21 @@ export const settings: ts.CompilerOptions = { export interface EmitterOptions { 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 { - private angularSourcePath: string|undefined; - private nodeModulesPath: string|undefined; private addedFiles = new Map(); private writtenFiles = new Map(); private scriptNames: string[]; @@ -56,19 +72,18 @@ export class EmittingCompilerHost implements ts.CompilerHost { private collector = new MetadataCollector(); constructor(scriptNames: string[], private options: EmitterOptions) { - const moduleFilename = module.filename.replace(/\\/g, '/'); - const 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'); + // Rewrite references to scripts with '@angular' to its corresponding location in + // the source tree. + this.scriptNames = scriptNames.map(f => this.effectiveName(f)); + this.root = rootPath; + } - // Rewrite references to scripts with '@angular' to its corresponding location in - // the source tree. - this.scriptNames = scriptNames.map(f => this.effectiveName(f)); - - this.root = root; - } + public writtenAngularFiles(target = new Map()): Map { + this.written.forEach((value, key) => { + const path = `/node_modules/@angular${key.substring(angularSourcePath.length)}`; + target.set(path, value); + }); + return target; } public addScript(fileName: string, content: string) { @@ -97,7 +112,7 @@ export class EmittingCompilerHost implements ts.CompilerHost { public effectiveName(fileName: string): string { const prefix = '@angular/'; return fileName.startsWith('@angular/') ? - path.join(this.angularSourcePath, fileName.substr(prefix.length)) : + path.join(angularSourcePath, fileName.substr(prefix.length)) : fileName; } @@ -171,31 +186,17 @@ export class EmittingCompilerHost implements ts.CompilerHost { getNewLine(): string { return '\n'; } } -const MOCK_NODEMODULES_PREFIX = '/node_modules/'; - export class MockCompilerHost implements ts.CompilerHost { scriptNames: string[]; - private angularSourcePath: string|undefined; - private nodeModulesPath: string|undefined; public overrides = new Map(); public writtenFiles = new Map(); private sourceFiles = new Map(); private assumeExists = new Set(); private traces: string[] = []; - constructor( - scriptNames: string[], private data: MockData, private angular: Map, - private libraries?: Map[]) { + constructor(scriptNames: string[], private data: MockDirectory) { 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 @@ -234,22 +235,13 @@ export class MockCompilerHost implements ts.CompilerHost { const effectiveName = this.getEffectiveName(fileName); if (effectiveName == fileName) { 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; } else { if (fileName.match(rxjs)) { let result = fs.existsSync(effectiveName); return result; } - const result = this.angular.has(effectiveName); - return result; + return false; } } @@ -315,12 +307,6 @@ export class MockCompilerHost implements ts.CompilerHost { let effectiveName = this.getEffectiveName(fileName); if (effectiveName === fileName) { 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; } else { if (fileName.match(rxjs)) { @@ -328,22 +314,16 @@ export class MockCompilerHost implements ts.CompilerHost { return fs.readFileSync(fileName, 'utf8'); } } - return this.angular.get(effectiveName); } } } private getEffectiveName(name: string): string { const node_modules = 'node_modules'; - const at_angular = '/@angular'; const rxjs = '/rxjs'; if (name.startsWith('/' + node_modules)) { - if (this.angularSourcePath && name.startsWith('/' + node_modules + at_angular)) { - return path.join( - 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)); + if (nodeModulesPath && name.startsWith('/' + node_modules + rxjs)) { + return path.join(nodeModulesPath, name.substr(node_modules.length + 1)); } } 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; let names = fileName.split('/'); if (names.length && !names[0].length) names.shift(); - let current: MockData|undefined = data; + let current: MockFileOrDirectory|undefined = data; for (let name of names) { if (typeof current === 'string') return undefined; @@ -454,7 +435,7 @@ function find(fileName: string, data: MockData | undefined): MockData|undefined 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); if (typeof result === 'string') { return result; @@ -462,7 +443,204 @@ function open(fileName: string, data: MockData | undefined): string|undefined { return undefined; } -function directoryExists(dirname: string, data: MockData | undefined): boolean { +function directoryExists(dirname: string, data: MockFileOrDirectory | undefined): boolean { let result = find(dirname, data); return !!result && typeof result !== 'string'; } + +export type MockFileArray = { + fileName: string, + content: string +}[]; + +export type MockData = MockDirectory | Map| (MockDirectory | Map)[]; + +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, target: MockFileArray) { + files.forEach((content, fileName) => { target.push({fileName, content}); }); +} + +export function arrayToMockMap(arr: MockFileArray): Map { + const map = new Map(); + 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 = 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(); + + 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}; + }); + }); +}