From 6665d76fbb37ce4248a3ed67064514ecf3c4093a Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Tue, 19 Sep 2017 11:43:34 -0700 Subject: [PATCH] perf(compiler): speed up watch mode (#19275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - don’t regenerate code for .d.ts files when an oldProgram is passed to `createProgram` - cache `fileExists` / `getSourceFile` / `readFile` in watch mode - refactor tests to share common code in `test_support` - support `—diagnostic` command line to print total time used per watch mode compilation. PR Close #19275 --- packages/compiler-cli/src/compiler_host.ts | 8 +- packages/compiler-cli/src/ngtools_api2.ts | 4 +- packages/compiler-cli/src/perform_watch.ts | 118 ++++-- packages/compiler-cli/src/transformers/api.ts | 11 + .../src/transformers/compiler_host.ts | 67 ++- .../compiler-cli/src/transformers/program.ts | 34 +- .../test/diagnostics/check_types_spec.ts | 65 +-- .../compiler-cli/test/perform_watch_spec.ts | 165 ++++++++ packages/compiler-cli/test/test_support.ts | 97 ++++- .../test/transformers/compiler_host_spec.ts | 2 +- .../test/transformers/program_spec.ts | 397 +++++++----------- 11 files changed, 622 insertions(+), 346 deletions(-) create mode 100644 packages/compiler-cli/test/perform_watch_spec.ts diff --git a/packages/compiler-cli/src/compiler_host.ts b/packages/compiler-cli/src/compiler_host.ts index 903be05092..ae274f5745 100644 --- a/packages/compiler-cli/src/compiler_host.ts +++ b/packages/compiler-cli/src/compiler_host.ts @@ -260,7 +260,7 @@ export class CompilerHost extends BaseAotCompilerHost { this.urlResolver = createOfflineCompileUrlResolver(); } - getMetadataForSourceFile(filePath: string): ModuleMetadata|undefined { + protected getSourceFile(filePath: string): ts.SourceFile { let sf = this.program.getSourceFile(filePath); if (!sf) { if (this.context.fileExists(filePath)) { @@ -270,7 +270,11 @@ export class CompilerHost extends BaseAotCompilerHost { throw new Error(`Source file ${filePath} not present in program.`); } } - return this.metadataProvider.getMetadata(sf); + return sf; + } + + getMetadataForSourceFile(filePath: string): ModuleMetadata|undefined { + return this.metadataProvider.getMetadata(this.getSourceFile(filePath)); } toSummaryFileName(fileName: string, referringSrcFileName: string): string { diff --git a/packages/compiler-cli/src/ngtools_api2.ts b/packages/compiler-cli/src/ngtools_api2.ts index 7b6c36b536..99fdd79b8e 100644 --- a/packages/compiler-cli/src/ngtools_api2.ts +++ b/packages/compiler-cli/src/ngtools_api2.ts @@ -66,8 +66,9 @@ export interface CompilerHost extends ts.CompilerHost { export enum EmitFlags { DTS = 1 << 0, JS = 1 << 1, + Codegen = 1 << 4, - Default = DTS | JS + Default = DTS | JS | Codegen } export interface CustomTransformers { @@ -106,6 +107,7 @@ export interface Program { customTransformers?: CustomTransformers, emitCallback?: TsEmitCallback }): ts.EmitResult; + getLibrarySummaries(): {fileName: string, content: string}[]; } // Wrapper for createProgram. diff --git a/packages/compiler-cli/src/perform_watch.ts b/packages/compiler-cli/src/perform_watch.ts index 9f07f90a25..c83ae86b21 100644 --- a/packages/compiler-cli/src/perform_watch.ts +++ b/packages/compiler-cli/src/perform_watch.ts @@ -35,9 +35,25 @@ const ChangeDiagnostics = { }, }; +function totalCompilationTimeDiagnostic(timeInMillis: number): api.Diagnostic { + let duration: string; + if (timeInMillis > 1000) { + duration = `${(timeInMillis / 1000).toPrecision(2)}s`; + } else { + duration = `${timeInMillis}ms`; + } + return { + category: ts.DiagnosticCategory.Message, + messageText: `Total time: ${duration}`, + code: api.DEFAULT_ERROR_CODE, + source: api.SOURCE, + }; +} + export enum FileChangeEvent { Change, - CreateDelete + CreateDelete, + CreateDeleteDir, } export interface PerformWatchHost { @@ -45,8 +61,9 @@ export interface PerformWatchHost { readConfiguration(): ParsedConfiguration; createCompilerHost(options: api.CompilerOptions): api.CompilerHost; createEmitCallback(options: api.CompilerOptions): api.TsEmitCallback|undefined; - onFileChange(listener: (event: FileChangeEvent, fileName: string) => void): - {close: () => void, ready: (cb: () => void) => void}; + onFileChange( + options: api.CompilerOptions, listener: (event: FileChangeEvent, fileName: string) => void, + ready: () => void): {close: () => void}; setTimeout(callback: () => void, ms: number): any; clearTimeout(timeoutId: any): void; } @@ -60,23 +77,17 @@ export function createPerformWatchHost( createCompilerHost: options => createCompilerHost({options}), readConfiguration: () => readConfiguration(configFileName, existingOptions), createEmitCallback: options => createEmitCallback ? createEmitCallback(options) : undefined, - onFileChange: (listeners) => { - const parsed = readConfiguration(configFileName, existingOptions); - function stubReady(cb: () => void) { process.nextTick(cb); } - if (parsed.errors && parsed.errors.length) { - reportDiagnostics(parsed.errors); - return {close: () => {}, ready: stubReady}; - } - if (!parsed.options.basePath) { + onFileChange: (options, listener, ready: () => void) => { + if (!options.basePath) { reportDiagnostics([{ category: ts.DiagnosticCategory.Error, messageText: 'Invalid configuration option. baseDir not specified', source: api.SOURCE, code: api.DEFAULT_ERROR_CODE }]); - return {close: () => {}, ready: stubReady}; + return {close: () => {}}; } - const watcher = chokidar.watch(parsed.options.basePath, { + const watcher = chokidar.watch(options.basePath, { // ignore .dotfiles, .js and .map files. // can't ignore other files as we e.g. want to recompile if an `.html` file changes as well. ignored: /((^[\/\\])\..)|(\.js$)|(\.map$)|(\.metadata\.json)/, @@ -86,15 +97,19 @@ export function createPerformWatchHost( watcher.on('all', (event: string, path: string) => { switch (event) { case 'change': - listeners(FileChangeEvent.Change, path); + listener(FileChangeEvent.Change, path); break; case 'unlink': case 'add': - listeners(FileChangeEvent.CreateDelete, path); + listener(FileChangeEvent.CreateDelete, path); + break; + case 'unlinkDir': + case 'addDir': + listener(FileChangeEvent.CreateDeleteDir, path); break; } }); - function ready(cb: () => void) { watcher.on('ready', cb); } + watcher.on('ready', ready); return {close: () => watcher.close(), ready}; }, setTimeout: (ts.sys.clearTimeout && ts.sys.setTimeout) || setTimeout, @@ -102,6 +117,12 @@ export function createPerformWatchHost( }; } +interface CacheEntry { + exists?: boolean; + sf?: ts.SourceFile; + content?: string; +} + /** * The logic in this function is adapted from `tsc.ts` from TypeScript. */ @@ -112,16 +133,30 @@ export function performWatchCompilation(host: PerformWatchHost): let cachedOptions: ParsedConfiguration|undefined; // CompilerOptions cached from last compilation let timerHandleForRecompilation: any; // Handle for 0.25s wait timer to trigger recompilation - // Watch basePath, ignoring .dotfiles - const fileWatcher = host.onFileChange(watchedFileChanged); const ingoreFilesForWatch = new Set(); + const fileCache = new Map(); const firstCompileResult = doCompilation(); - const readyPromise = new Promise(resolve => fileWatcher.ready(resolve)); + // Watch basePath, ignoring .dotfiles + let resolveReadyPromise: () => void; + const readyPromise = new Promise(resolve => resolveReadyPromise = resolve); + // Note: ! is ok as options are filled after the first compilation + // Note: ! is ok as resolvedReadyPromise is filled by the previous call + const fileWatcher = + host.onFileChange(cachedOptions !.options, watchedFileChanged, resolveReadyPromise !); return {close, ready: cb => readyPromise.then(cb), firstCompileResult}; + function cacheEntry(fileName: string): CacheEntry { + let entry = fileCache.get(fileName); + if (!entry) { + entry = {}; + fileCache.set(fileName, entry); + } + return entry; + } + function close() { fileWatcher.close(); if (timerHandleForRecompilation) { @@ -139,11 +174,8 @@ export function performWatchCompilation(host: PerformWatchHost): host.reportDiagnostics(cachedOptions.errors); return cachedOptions.errors; } + const startTime = Date.now(); if (!cachedCompilerHost) { - // TODO(chuckj): consider avoiding re-generating factories for libraries. - // Consider modifying the AotCompilerHost to be able to remember the summary files - // generated from previous compiliations and return false from isSourceFile for - // .d.ts files for which a summary file was already generated.å cachedCompilerHost = host.createCompilerHost(cachedOptions.options); const originalWriteFileCallback = cachedCompilerHost.writeFile; cachedCompilerHost.writeFile = function( @@ -152,6 +184,31 @@ export function performWatchCompilation(host: PerformWatchHost): ingoreFilesForWatch.add(path.normalize(fileName)); return originalWriteFileCallback(fileName, data, writeByteOrderMark, onError, sourceFiles); }; + const originalFileExists = cachedCompilerHost.fileExists; + cachedCompilerHost.fileExists = function(fileName: string) { + const ce = cacheEntry(fileName); + if (ce.exists == null) { + ce.exists = originalFileExists.call(this, fileName); + } + return ce.exists !; + }; + const originalGetSourceFile = cachedCompilerHost.getSourceFile; + cachedCompilerHost.getSourceFile = function( + fileName: string, languageVersion: ts.ScriptTarget) { + const ce = cacheEntry(fileName); + if (!ce.sf) { + ce.sf = originalGetSourceFile.call(this, fileName, languageVersion); + } + return ce.sf !; + }; + const originalReadFile = cachedCompilerHost.readFile; + cachedCompilerHost.readFile = function(fileName: string) { + const ce = cacheEntry(fileName); + if (ce.content == null) { + ce.content = originalReadFile.call(this, fileName); + } + return ce.content !; + }; } ingoreFilesForWatch.clear(); const compileResult = performCompilation({ @@ -166,6 +223,11 @@ export function performWatchCompilation(host: PerformWatchHost): host.reportDiagnostics(compileResult.diagnostics); } + const endTime = Date.now(); + if (cachedOptions.options.diagnostics) { + const totalTime = (endTime - startTime) / 1000; + host.reportDiagnostics([totalCompilationTimeDiagnostic(endTime - startTime)]); + } const exitCode = exitCodeFromResult(compileResult.diagnostics); if (exitCode == 0) { cachedProgram = compileResult.program; @@ -191,11 +253,19 @@ export function performWatchCompilation(host: PerformWatchHost): path.normalize(fileName) === path.normalize(cachedOptions.project)) { // If the configuration file changes, forget everything and start the recompilation timer resetOptions(); - } else if (event === FileChangeEvent.CreateDelete) { + } else if ( + event === FileChangeEvent.CreateDelete || event === FileChangeEvent.CreateDeleteDir) { // If a file was added or removed, reread the configuration // to determine the new list of root files. cachedOptions = undefined; } + + if (event === FileChangeEvent.CreateDeleteDir) { + fileCache.clear(); + } else { + fileCache.delete(fileName); + } + if (!ingoreFilesForWatch.has(path.normalize(fileName))) { // Ignore the file if the file is one that was written by the compiler. startTimerForRecompilation(); diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index eed1d0f9af..13f9081dcc 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -30,6 +30,11 @@ export function isNgDiagnostic(diagnostic: any): diagnostic is Diagnostic { } export interface CompilerOptions extends ts.CompilerOptions { + // Write statistics about compilation (e.g. total time, ...) + // Note: this is the --diagnostics command line option from TS (which is @internal + // on ts.CompilerOptions interface). + diagnostics?: boolean; + // Absolute path to a directory where generated file structure is written. // If unspecified, generated files will be written alongside sources. // @deprecated - no effect @@ -273,4 +278,10 @@ export interface Program { customTransformers?: CustomTransformers, emitCallback?: TsEmitCallback }): ts.EmitResult; + + /** + * Returns the .ngsummary.json files of libraries that have been compiled + * in this program or previous programs. + */ + getLibrarySummaries(): {fileName: string, content: string}[]; } diff --git a/packages/compiler-cli/src/transformers/compiler_host.ts b/packages/compiler-cli/src/transformers/compiler_host.ts index 0e3f3c2831..90caee9df3 100644 --- a/packages/compiler-cli/src/transformers/compiler_host.ts +++ b/packages/compiler-cli/src/transformers/compiler_host.ts @@ -61,7 +61,8 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends constructor( private rootFiles: string[], options: CompilerOptions, context: CompilerHost, private metadataProvider: MetadataProvider, - private codeGenerator: (fileName: string) => GeneratedFile[]) { + private codeGenerator: (fileName: string) => GeneratedFile[], + private summariesFromPreviousCompilations: Map) { super(options, context); this.moduleResolutionCache = ts.createModuleResolutionCache( this.context.getCurrentDirectory !(), this.context.getCanonicalFileName.bind(this.context)); @@ -292,7 +293,8 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends } this.generatedCodeFor.add(fileName); - const baseNameFromGeneratedFile = this._getBaseNameForGeneratedFile(fileName); + const baseNameFromGeneratedFile = this._getBaseNamesForGeneratedFile(fileName).find( + fileName => this.isSourceFile(fileName) && this.fileExists(fileName)); if (baseNameFromGeneratedFile) { return this.ensureCodeGeneratedFor(baseNameFromGeneratedFile); } @@ -336,29 +338,58 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends fileName = stripNgResourceSuffix(fileName); // Note: Don't rely on this.generatedSourceFiles here, // as it might not have been filled yet. - if (this._getBaseNameForGeneratedFile(fileName)) { + if (this._getBaseNamesForGeneratedFile(fileName).find(baseFileName => { + if (this.isSourceFile(baseFileName)) { + return this.fileExists(baseFileName); + } else { + // Note: the factories of a previous program + // are not reachable via the regular fileExists + // as they might be in the outDir. So we derive their + // fileExist information based on the .ngsummary.json file. + return this.fileExists(summaryFileName(baseFileName)); + } + })) { return true; } - return this.originalSourceFiles.has(fileName) || this.context.fileExists(fileName); + return this.summariesFromPreviousCompilations.has(fileName) || + this.originalSourceFiles.has(fileName) || this.context.fileExists(fileName); } - private _getBaseNameForGeneratedFile(genFileName: string): string|null { + private _getBaseNamesForGeneratedFile(genFileName: string): string[] { const genMatch = GENERATED_FILES.exec(genFileName); if (genMatch) { const [, base, genSuffix, suffix] = genMatch; - // Note: on-the-fly generated files always have a `.ts` suffix, - // but the file from which we generated it can be a `.ts`/ `.d.ts` - // (see options.generateCodeForLibraries). - // It can also be a `.css` file in case of a `.css.ngstyle.ts` file - if (suffix === 'ts') { - const baseNames = - genSuffix.indexOf('ngstyle') >= 0 ? [base] : [`${base}.ts`, `${base}.d.ts`]; - return baseNames.find( - baseName => this.isSourceFile(baseName) && this.fileExists(baseName)) || - null; + let baseNames: string[] = []; + if (genSuffix.indexOf('ngstyle') >= 0) { + // Note: ngstlye files have names like `afile.css.ngstyle.ts` + baseNames = [base]; + } else if (suffix === 'd.ts') { + baseNames = [base + '.d.ts']; + } else if (suffix === 'ts') { + // Note: on-the-fly generated files always have a `.ts` suffix, + // but the file from which we generated it can be a `.ts`/ `.d.ts` + // (see options.generateCodeForLibraries). + baseNames = [`${base}.ts`, `${base}.d.ts`]; } + return baseNames; } - return null; + return []; + } + + loadSummary(filePath: string): string|null { + if (this.summariesFromPreviousCompilations.has(filePath)) { + return this.summariesFromPreviousCompilations.get(filePath) !; + } + return super.loadSummary(filePath); + } + + isSourceFile(filePath: string): boolean { + // If we have a summary from a previous compilation, + // treat the file never as a source file. + if (this.summariesFromPreviousCompilations.has(summaryFileName(filePath))) { + return false; + } + return super.isSourceFile(filePath); } readFile = (fileName: string) => this.context.readFile(fileName); @@ -431,3 +462,7 @@ function stripNgResourceSuffix(fileName: string): string { function addNgResourceSuffix(fileName: string): string { return `${fileName}.$ngresource$`; } + +function summaryFileName(fileName: string): string { + return fileName.replace(EXT, '') + '.ngsummary.json'; +} \ No newline at end of file diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index c256645dcb..54cc9ed527 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -35,6 +35,9 @@ const defaultEmitCallback: TsEmitCallback = class AngularCompilerProgram implements Program { private metadataCache: LowerMetadataCache; + private summariesFromPreviousCompilations = new Map(); + // Note: This will be cleared out as soon as we create the _tsProgram + private oldTsProgram: ts.Program|undefined; private _emittedGenFiles: GeneratedFile[]|undefined; // Lazily initialized fields @@ -54,6 +57,11 @@ class AngularCompilerProgram implements Program { if (Number(major) < 2 || (Number(major) === 2 && Number(minor) < 4)) { throw new Error('The Angular Compiler requires TypeScript >= 2.4.'); } + this.oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined; + if (oldProgram) { + oldProgram.getLibrarySummaries().forEach( + ({content, fileName}) => this.summariesFromPreviousCompilations.set(fileName, content)); + } this.rootNames = rootNames = rootNames.filter(r => !GENERATED_FILES.test(r)); if (options.flatModuleOutFile) { @@ -75,6 +83,23 @@ class AngularCompilerProgram implements Program { this.metadataCache = new LowerMetadataCache({quotedNames: true}, !!options.strictMetadataEmit); } + getLibrarySummaries(): {fileName: string, content: string}[] { + const emittedLibSummaries: {fileName: string, content: string}[] = []; + this.summariesFromPreviousCompilations.forEach( + (content, fileName) => emittedLibSummaries.push({fileName, content})); + if (this._emittedGenFiles) { + this._emittedGenFiles.forEach(genFile => { + if (genFile.srcFileUrl.endsWith('.d.ts') && + genFile.genFileUrl.endsWith('.ngsummary.json')) { + // Note: ! is ok here as ngsummary.json files are always plain text, so genFile.source + // is filled. + emittedLibSummaries.push({fileName: genFile.genFileUrl, content: genFile.source !}); + } + }); + } + return emittedLibSummaries; + } + getTsProgram(): ts.Program { return this.tsProgram; } getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken) { @@ -284,6 +309,9 @@ class AngularCompilerProgram implements Program { if (this._analyzedModules) { throw new Error(`Internal Error: already initalized!`); } + // Note: This is important to not produce a memory leak! + const oldTsProgram = this.oldTsProgram; + this.oldTsProgram = undefined; const analyzedFiles: NgAnalyzedFile[] = []; const codegen = (fileName: string) => { if (this._analyzedModules) { @@ -295,15 +323,13 @@ class AngularCompilerProgram implements Program { return this._compiler.emitBasicStubs(analyzedFile); }; const hostAdapter = new TsCompilerAotCompilerTypeCheckHostAdapter( - this.rootNames, this.options, this.host, this.metadataCache, codegen); + this.rootNames, this.options, this.host, this.metadataCache, codegen, + this.summariesFromPreviousCompilations); const aotOptions = getAotCompilerOptions(this.options); this._compiler = createAotCompiler(hostAdapter, aotOptions).compiler; this._typeCheckHost = hostAdapter; this._structuralDiagnostics = []; - const oldTsProgram = this.oldProgram ? this.oldProgram.getTsProgram() : undefined; - // Note: This is important to not produce a memory leak! - this.oldProgram = undefined; const tmpProgram = ts.createProgram(this.rootNames, this.options, hostAdapter, oldTsProgram); return {tmpProgram, analyzedFiles, hostAdapter}; } diff --git a/packages/compiler-cli/test/diagnostics/check_types_spec.ts b/packages/compiler-cli/test/diagnostics/check_types_spec.ts index a7d505a6f3..8acb3604df 100644 --- a/packages/compiler-cli/test/diagnostics/check_types_spec.ts +++ b/packages/compiler-cli/test/diagnostics/check_types_spec.ts @@ -7,84 +7,41 @@ */ import * as ng from '@angular/compiler-cli'; -import {makeTempDir} from '@angular/tsc-wrapped/test/test_support'; import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; -function getNgRootDir() { - const moduleFilename = module.filename.replace(/\\/g, '/'); - const distIndex = moduleFilename.indexOf('/dist/all'); - return moduleFilename.substr(0, distIndex); -} +import {TestSupport, expectNoDiagnostics, setup} from '../test_support'; describe('ng type checker', () => { - let basePath: string; - let write: (fileName: string, content: string) => void; let errorSpy: jasmine.Spy&((s: string) => void); + let testSupport: TestSupport; function compileAndCheck( mockDirs: {[fileName: string]: string}[], overrideOptions: ng.CompilerOptions = {}): ng.Diagnostics { + testSupport.writeFiles(...mockDirs); const fileNames: string[] = []; mockDirs.forEach((dir) => { Object.keys(dir).forEach((fileName) => { if (fileName.endsWith('.ts')) { - fileNames.push(path.resolve(basePath, fileName)); + fileNames.push(path.resolve(testSupport.basePath, fileName)); } - write(fileName, dir[fileName]); }); }); - const options: ng.CompilerOptions = { - basePath, - 'experimentalDecorators': true, - 'skipLibCheck': true, - 'strict': true, - 'types': [], - 'outDir': path.resolve(basePath, 'built'), - 'rootDir': basePath, - 'baseUrl': basePath, - 'declaration': true, - 'target': ts.ScriptTarget.ES5, - 'module': ts.ModuleKind.ES2015, - 'moduleResolution': ts.ModuleResolutionKind.NodeJs, - 'lib': [ - path.resolve(basePath, 'node_modules/typescript/lib/lib.es6.d.ts'), - path.resolve(basePath, 'node_modules/typescript/lib/lib.dom.d.ts') - ], - 'typeRoots': [path.resolve(basePath, 'node_modules/@types')], ...overrideOptions - }; + const options = testSupport.createCompilerOptions(overrideOptions); const {diagnostics} = ng.performCompilation({rootNames: fileNames, options}); return diagnostics; } beforeEach(() => { errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error); - basePath = makeTempDir(); - write = (fileName: string, content: string) => { - const dir = path.dirname(fileName); - if (dir != '.') { - const newDir = path.join(basePath, dir); - if (!fs.existsSync(newDir)) fs.mkdirSync(newDir); - } - fs.writeFileSync(path.join(basePath, fileName), content, {encoding: 'utf-8'}); - }; - const ngRootDir = getNgRootDir(); - const nodeModulesPath = path.resolve(basePath, 'node_modules'); - fs.mkdirSync(nodeModulesPath); - fs.symlinkSync( - path.resolve(ngRootDir, 'dist', 'all', '@angular'), - path.resolve(nodeModulesPath, '@angular')); - fs.symlinkSync( - path.resolve(ngRootDir, 'node_modules', 'rxjs'), path.resolve(nodeModulesPath, 'rxjs')); - fs.symlinkSync( - path.resolve(ngRootDir, 'node_modules', 'typescript'), - path.resolve(nodeModulesPath, 'typescript')); + testSupport = setup(); }); function accept( files: {[fileName: string]: string} = {}, overrideOptions: ng.CompilerOptions = {}) { - expectNoDiagnostics(compileAndCheck([QUICKSTART, files], overrideOptions)); + expectNoDiagnostics({}, compileAndCheck([QUICKSTART, files], overrideOptions)); } function reject( @@ -193,7 +150,7 @@ describe('ng type checker', () => { describe('with lowered expressions', () => { it('should not report lowered expressions as errors', - () => { expectNoDiagnostics(compileAndCheck([LOWERING_QUICKSTART])); }); + () => { expectNoDiagnostics({}, compileAndCheck([LOWERING_QUICKSTART])); }); }); }); @@ -284,9 +241,3 @@ const LOWERING_QUICKSTART = { export class AppModule { } ` }; - -function expectNoDiagnostics(diagnostics: ng.Diagnostics) { - if (diagnostics && diagnostics.length) { - throw new Error(ng.formatDiagnostics({}, diagnostics)); - } -} diff --git a/packages/compiler-cli/test/perform_watch_spec.ts b/packages/compiler-cli/test/perform_watch_spec.ts new file mode 100644 index 0000000000..767eec82fd --- /dev/null +++ b/packages/compiler-cli/test/perform_watch_spec.ts @@ -0,0 +1,165 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; + +import * as ng from '../index'; +import {FileChangeEvent, performWatchCompilation} from '../src/perform_watch'; + +import {TestSupport, expectNoDiagnostics, setup} from './test_support'; + +describe('perform watch', () => { + let testSupport: TestSupport; + let outDir: string; + + beforeEach(() => { + testSupport = setup(); + outDir = path.resolve(testSupport.basePath, 'outDir'); + }); + + function createConfig(): ng.ParsedConfiguration { + const options = testSupport.createCompilerOptions({outDir}); + return { + options, + rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')], + project: path.resolve(testSupport.basePath, 'src/tsconfig.json'), + emitFlags: ng.EmitFlags.Default, + errors: [] + }; + } + + it('should compile files during the initial run', () => { + const config = createConfig(); + const host = new MockWatchHost(config); + + testSupport.writeFiles({ + 'src/main.ts': createModuleAndCompSource('main'), + 'src/index.ts': `export * from './main'; `, + }); + + const watchResult = performWatchCompilation(host); + expectNoDiagnostics(config.options, watchResult.firstCompileResult); + + expect(fs.existsSync(path.resolve(outDir, 'src', 'main.ngfactory.js'))).toBe(true); + }); + + it('should cache files on subsequent runs', () => { + const config = createConfig(); + const host = new MockWatchHost(config); + let fileExistsSpy: jasmine.Spy; + let getSourceFileSpy: jasmine.Spy; + host.createCompilerHost = (options: ng.CompilerOptions) => { + const ngHost = ng.createCompilerHost({options}); + fileExistsSpy = spyOn(ngHost, 'fileExists').and.callThrough(); + getSourceFileSpy = spyOn(ngHost, 'getSourceFile').and.callThrough(); + return ngHost; + }; + + testSupport.writeFiles({ + 'src/main.ts': createModuleAndCompSource('main'), + 'src/util.ts': `export const x = 1;`, + 'src/index.ts': ` + export * from './main'; + export * from './util'; + `, + }); + + const mainTsPath = path.resolve(testSupport.basePath, 'src', 'main.ts'); + const utilTsPath = path.resolve(testSupport.basePath, 'src', 'util.ts'); + const mainNgFactory = path.resolve(outDir, 'src', 'main.ngfactory.js'); + performWatchCompilation(host); + expect(fs.existsSync(mainNgFactory)).toBe(true); + expect(fileExistsSpy !).toHaveBeenCalledWith(mainTsPath); + expect(fileExistsSpy !).toHaveBeenCalledWith(utilTsPath); + expect(getSourceFileSpy !).toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5); + expect(getSourceFileSpy !).toHaveBeenCalledWith(utilTsPath, ts.ScriptTarget.ES5); + + fileExistsSpy !.calls.reset(); + getSourceFileSpy !.calls.reset(); + + // trigger a single file change + // -> all other files should be cached + fs.unlinkSync(mainNgFactory); + host.triggerFileChange(FileChangeEvent.Change, utilTsPath); + + expect(fs.existsSync(mainNgFactory)).toBe(true); + expect(fileExistsSpy !).not.toHaveBeenCalledWith(mainTsPath); + expect(fileExistsSpy !).toHaveBeenCalledWith(utilTsPath); + expect(getSourceFileSpy !).not.toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5); + expect(getSourceFileSpy !).toHaveBeenCalledWith(utilTsPath, ts.ScriptTarget.ES5); + + // trigger a folder change + // -> nothing should be cached + fs.unlinkSync(mainNgFactory); + host.triggerFileChange( + FileChangeEvent.CreateDeleteDir, path.resolve(testSupport.basePath, 'src')); + + expect(fs.existsSync(mainNgFactory)).toBe(true); + expect(fileExistsSpy !).toHaveBeenCalledWith(mainTsPath); + expect(fileExistsSpy !).toHaveBeenCalledWith(utilTsPath); + expect(getSourceFileSpy !).toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5); + expect(getSourceFileSpy !).toHaveBeenCalledWith(utilTsPath, ts.ScriptTarget.ES5); + }); +}); + +function createModuleAndCompSource(prefix: string, template: string = prefix + 'template') { + const templateEntry = + template.endsWith('.html') ? `templateUrl: '${template}'` : `template: \`${template}\``; + return ` + import {Component, NgModule} from '@angular/core'; + + @Component({selector: '${prefix}', ${templateEntry}}) + export class ${prefix}Comp {} + + @NgModule({declarations: [${prefix}Comp]}) + export class ${prefix}Module {} + `; +} + +class MockWatchHost { + timeoutListeners: Array<(() => void)|null> = []; + fileChangeListeners: Array<((event: FileChangeEvent, fileName: string) => void)|null> = []; + diagnostics: ng.Diagnostics = []; + constructor(public config: ng.ParsedConfiguration) {} + + reportDiagnostics(diags: ng.Diagnostics) { this.diagnostics.push(...diags); } + readConfiguration() { return this.config; } + createCompilerHost(options: ng.CompilerOptions) { return ng.createCompilerHost({options}); }; + createEmitCallback() { return undefined; } + onFileChange( + options: ng.CompilerOptions, listener: (event: FileChangeEvent, fileName: string) => void, + ready: () => void) { + const id = this.fileChangeListeners.length; + this.fileChangeListeners.push(listener); + ready(); + return { + close: () => this.fileChangeListeners[id] = null, + }; + } + setTimeout(callback: () => void, ms: number): any { + const id = this.timeoutListeners.length; + this.timeoutListeners.push(callback); + return id; + } + clearTimeout(timeoutId: any): void { this.timeoutListeners[timeoutId] = null; } + flushTimeouts() { + this.timeoutListeners.forEach(cb => { + if (cb) cb(); + }); + } + triggerFileChange(event: FileChangeEvent, fileName: string) { + this.fileChangeListeners.forEach(listener => { + if (listener) { + listener(event, fileName); + } + }); + this.flushTimeouts(); + } +} diff --git a/packages/compiler-cli/test/test_support.ts b/packages/compiler-cli/test/test_support.ts index feb27463bd..8845e2275d 100644 --- a/packages/compiler-cli/test/test_support.ts +++ b/packages/compiler-cli/test/test_support.ts @@ -9,9 +9,17 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import * as ts from 'typescript'; +import * as ng from '../index'; const tmpdir = process.env.TEST_TMPDIR || os.tmpdir(); +function getNgRootDir() { + const moduleFilename = module.filename.replace(/\\/g, '/'); + const distIndex = moduleFilename.indexOf('/dist/all'); + return moduleFilename.substr(0, distIndex); +} + export function writeTempFile(name: string, contents: string): string { // TEST_TMPDIR is set by bazel. const id = (Math.random() * 1000000).toFixed(0); @@ -25,4 +33,91 @@ export function makeTempDir(): string { const dir = path.join(tmpdir, `tmp.${id}`); fs.mkdirSync(dir); return dir; -} \ No newline at end of file +} + +export interface TestSupport { + basePath: string; + write(fileName: string, content: string): void; + writeFiles(...mockDirs: {[fileName: string]: string}[]): void; + createCompilerOptions(overrideOptions?: ng.CompilerOptions): ng.CompilerOptions; + shouldExist(fileName: string): void; + shouldNotExist(fileName: string): void; +} + +export function setup(): TestSupport { + const basePath = makeTempDir(); + + const ngRootDir = getNgRootDir(); + const nodeModulesPath = path.resolve(basePath, 'node_modules'); + fs.mkdirSync(nodeModulesPath); + fs.symlinkSync( + path.resolve(ngRootDir, 'dist', 'all', '@angular'), + path.resolve(nodeModulesPath, '@angular')); + fs.symlinkSync( + path.resolve(ngRootDir, 'node_modules', 'rxjs'), path.resolve(nodeModulesPath, 'rxjs')); + fs.symlinkSync( + path.resolve(ngRootDir, 'node_modules', 'typescript'), + path.resolve(nodeModulesPath, 'typescript')); + + return {basePath, write, writeFiles, createCompilerOptions, shouldExist, shouldNotExist}; + + function write(fileName: string, content: string) { + const dir = path.dirname(fileName); + if (dir != '.') { + const newDir = path.join(basePath, dir); + if (!fs.existsSync(newDir)) fs.mkdirSync(newDir); + } + fs.writeFileSync(path.join(basePath, fileName), content, {encoding: 'utf-8'}); + } + + function writeFiles(...mockDirs: {[fileName: string]: string}[]) { + mockDirs.forEach( + (dir) => { Object.keys(dir).forEach((fileName) => { write(fileName, dir[fileName]); }); }); + } + + function createCompilerOptions(overrideOptions: ng.CompilerOptions = {}): ng.CompilerOptions { + return { + basePath, + 'experimentalDecorators': true, + 'skipLibCheck': true, + 'strict': true, + 'types': [], + 'outDir': path.resolve(basePath, 'built'), + 'rootDir': basePath, + 'baseUrl': basePath, + 'declaration': true, + 'target': ts.ScriptTarget.ES5, + 'module': ts.ModuleKind.ES2015, + 'moduleResolution': ts.ModuleResolutionKind.NodeJs, + 'lib': [ + path.resolve(basePath, 'node_modules/typescript/lib/lib.es6.d.ts'), + ], + ...overrideOptions, + }; + } + + function shouldExist(fileName: string) { + if (!fs.existsSync(path.resolve(basePath, fileName))) { + throw new Error(`Expected ${fileName} to be emitted (basePath: ${basePath})`); + } + } + + function shouldNotExist(fileName: string) { + if (fs.existsSync(path.resolve(basePath, fileName))) { + throw new Error(`Did not expect ${fileName} to be emitted (basePath: ${basePath})`); + } + } +} + +export function expectNoDiagnostics(options: ng.CompilerOptions, diags: ng.Diagnostics) { + if (diags.length) { + throw new Error(`Expected no diagnostics: ${ng.formatDiagnostics(options, diags)}`); + } +} + +export function expectNoDiagnosticsInProgram(options: ng.CompilerOptions, p: ng.Program) { + expectNoDiagnostics(options, [ + ...p.getNgStructuralDiagnostics(), ...p.getTsSemanticDiagnostics(), + ...p.getNgSemanticDiagnostics() + ]); +} diff --git a/packages/compiler-cli/test/transformers/compiler_host_spec.ts b/packages/compiler-cli/test/transformers/compiler_host_spec.ts index 5364dfa7ca..404040c31b 100644 --- a/packages/compiler-cli/test/transformers/compiler_host_spec.ts +++ b/packages/compiler-cli/test/transformers/compiler_host_spec.ts @@ -39,7 +39,7 @@ describe('NgCompilerHost', () => { ngHost = createNgHost({files}), }: {files?: Directory, options?: CompilerOptions, ngHost?: CompilerHost} = {}) { return new TsCompilerAotCompilerTypeCheckHostAdapter( - ['/tmp/index.ts'], options, ngHost, new MetadataCollector(), codeGenerator); + ['/tmp/index.ts'], options, ngHost, new MetadataCollector(), codeGenerator, new Map()); } describe('fileNameToModuleName', () => { diff --git a/packages/compiler-cli/test/transformers/program_spec.ts b/packages/compiler-cli/test/transformers/program_spec.ts index 65c1fba161..0ba960e608 100644 --- a/packages/compiler-cli/test/transformers/program_spec.ts +++ b/packages/compiler-cli/test/transformers/program_spec.ts @@ -7,288 +7,205 @@ */ import * as ng from '@angular/compiler-cli'; -import {makeTempDir} from '@angular/tsc-wrapped/test/test_support'; import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; -import {StructureIsReused, tsStructureIsReused} from '../../src/transformers/util'; - -function getNgRootDir() { - const moduleFilename = module.filename.replace(/\\/g, '/'); - const distIndex = moduleFilename.indexOf('/dist/all'); - return moduleFilename.substr(0, distIndex); -} +import {CompilerHost} from '../../src/transformers/api'; +import {GENERATED_FILES, StructureIsReused, tsStructureIsReused} from '../../src/transformers/util'; +import {TestSupport, expectNoDiagnosticsInProgram, setup} from '../test_support'; describe('ng program', () => { - let basePath: string; - let write: (fileName: string, content: string) => void; + let testSupport: TestSupport; let errorSpy: jasmine.Spy&((s: string) => void); - function writeFiles(...mockDirs: {[fileName: string]: string}[]) { - mockDirs.forEach( - (dir) => { Object.keys(dir).forEach((fileName) => { write(fileName, dir[fileName]); }); }); - } - - function createCompilerOptions(overrideOptions: ng.CompilerOptions = {}): ng.CompilerOptions { - return { - basePath, - 'experimentalDecorators': true, - 'skipLibCheck': true, - 'strict': true, - 'types': [], - 'outDir': path.resolve(basePath, 'built'), - 'rootDir': basePath, - 'baseUrl': basePath, - 'declaration': true, - 'target': ts.ScriptTarget.ES5, - 'module': ts.ModuleKind.ES2015, - 'moduleResolution': ts.ModuleResolutionKind.NodeJs, - 'lib': [ - path.resolve(basePath, 'node_modules/typescript/lib/lib.es6.d.ts'), - path.resolve(basePath, 'node_modules/typescript/lib/lib.dom.d.ts') - ], - 'typeRoots': [path.resolve(basePath, 'node_modules/@types')], ...overrideOptions, - }; - } - - function expectNoDiagnostics(options: ng.CompilerOptions, p: ng.Program) { - const diags: ng.Diagnostics = - [...p.getTsSemanticDiagnostics(), ...p.getNgSemanticDiagnostics()]; - if (diags.length > 0) { - console.error('Diagnostics: ' + ng.formatDiagnostics(options, diags)); - throw new Error('Expected no diagnostics.'); - } - } - beforeEach(() => { errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error); - basePath = makeTempDir(); - write = (fileName: string, content: string) => { - const dir = path.dirname(fileName); - if (dir != '.') { - const newDir = path.join(basePath, dir); - if (!fs.existsSync(newDir)) fs.mkdirSync(newDir); - } - fs.writeFileSync(path.join(basePath, fileName), content, {encoding: 'utf-8'}); - }; - const ngRootDir = getNgRootDir(); - const nodeModulesPath = path.resolve(basePath, 'node_modules'); - fs.mkdirSync(nodeModulesPath); - fs.symlinkSync( - path.resolve(ngRootDir, 'dist', 'all', '@angular'), - path.resolve(nodeModulesPath, '@angular')); - fs.symlinkSync( - path.resolve(ngRootDir, 'node_modules', 'rxjs'), path.resolve(nodeModulesPath, 'rxjs')); - fs.symlinkSync( - path.resolve(ngRootDir, 'node_modules', 'typescript'), - path.resolve(nodeModulesPath, 'typescript')); + testSupport = setup(); }); - describe('reuse of old ts program', () => { - const files = { - 'src/util.ts': `export const x = 1;`, - 'src/main.ts': ` - import {NgModule, Component} from '@angular/core'; - import {x} from './util'; + function createModuleAndCompSource(prefix: string, template: string = prefix + 'template') { + const templateEntry = + template.endsWith('.html') ? `templateUrl: '${template}'` : `template: \`${template}\``; + return ` + import {Component, NgModule} from '@angular/core'; - @Component({selector: 'comp', templateUrl: './main.html'}) - export class MyComp {} + @Component({selector: '${prefix}', ${templateEntry}}) + export class ${prefix}Comp {} - @NgModule() - export class MyModule {} - `, - 'src/main.html': `Hello world`, - }; + @NgModule({declarations: [${prefix}Comp]}) + export class ${prefix}Module {} + `; + } - function expectResuse(newFiles: {[fileName: string]: string}, reuseLevel: StructureIsReused) { - writeFiles(files); + describe('reuse of old program', () => { - const options1 = createCompilerOptions(); - const host1 = ng.createCompilerHost({options: options1}); - const rootNames1 = [path.resolve(basePath, 'src/main.ts')]; - - const p1 = ng.createProgram({rootNames: rootNames1, options: options1, host: host1}); - expectNoDiagnostics(options1, p1); - - // Note: we recreate the options, rootNames and the host - // to check that TS checks against values, and not references! - writeFiles(newFiles); - const options2 = {...options1}; - const host2 = ng.createCompilerHost({options: options2}); - const rootNames2 = [...rootNames1]; - - const p2 = - ng.createProgram({rootNames: rootNames2, options: options2, host: host2, oldProgram: p1}); - expectNoDiagnostics(options1, p2); - - expect(tsStructureIsReused(p1.getTsProgram())).toBe(reuseLevel); + function compileLib(libName: string) { + testSupport.writeFiles({ + [`${libName}_src/index.ts`]: createModuleAndCompSource(libName), + }); + const options = testSupport.createCompilerOptions({ + skipTemplateCodegen: true, + }); + const program = ng.createProgram({ + rootNames: [path.resolve(testSupport.basePath, `${libName}_src/index.ts`)], + options, + host: ng.createCompilerHost({options}), + }); + expectNoDiagnosticsInProgram(options, program); + fs.symlinkSync( + path.resolve(testSupport.basePath, 'built', `${libName}_src`), + path.resolve(testSupport.basePath, 'node_modules', libName)); + program.emit({emitFlags: ng.EmitFlags.DTS | ng.EmitFlags.JS | ng.EmitFlags.Metadata}); } - it('should reuse completely if nothing changed', - () => { expectResuse({}, StructureIsReused.Completely); }); + function compile(oldProgram?: ng.Program): ng.Program { + const options = testSupport.createCompilerOptions(); + const rootNames = [path.resolve(testSupport.basePath, 'src/index.ts')]; - it('should resuse if a template or a ts file changed', () => { - expectResuse( - { - 'src/main.html': `Some other text`, - 'src/util.ts': `export const x = 2;`, - }, - StructureIsReused.Completely); + const program = ng.createProgram({ + rootNames: rootNames, + options: testSupport.createCompilerOptions(), + host: ng.createCompilerHost({options}), oldProgram, + }); + expectNoDiagnosticsInProgram(options, program); + program.emit(); + return program; + } + + it('should reuse generated code for libraries from old programs', () => { + compileLib('lib'); + testSupport.writeFiles({ + 'src/main.ts': createModuleAndCompSource('main'), + 'src/index.ts': ` + export * from './main'; + export * from 'lib/index'; + ` + }); + const p1 = compile(); + expect(p1.getTsProgram().getSourceFiles().some( + sf => /node_modules\/lib\/.*\.ngfactory\.ts$/.test(sf.fileName))) + .toBe(true); + expect(p1.getTsProgram().getSourceFiles().some( + sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName))) + .toBe(false); + const p2 = compile(p1); + expect(p2.getTsProgram().getSourceFiles().some( + sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName))) + .toBe(false); + expect(p2.getTsProgram().getSourceFiles().some( + sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName))) + .toBe(false); + + // import a library for which we didn't generate code before + compileLib('lib2'); + testSupport.writeFiles({ + 'src/index.ts': ` + export * from './main'; + export * from 'lib/index'; + export * from 'lib2/index'; + `, + }); + const p3 = compile(p2); + expect(p3.getTsProgram().getSourceFiles().some( + sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName))) + .toBe(false); + expect(p3.getTsProgram().getSourceFiles().some( + sf => /node_modules\/lib2\/.*\.ngfactory\.ts$/.test(sf.fileName))) + .toBe(true); + + const p4 = compile(p3); + expect(p4.getTsProgram().getSourceFiles().some( + sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName))) + .toBe(false); + expect(p4.getTsProgram().getSourceFiles().some( + sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName))) + .toBe(false); }); - it('should not reuse if an import changed', () => { - expectResuse( - { - 'src/util.ts': ` - import {Injectable} from '@angular/core'; - export const x = 2; - `, - }, - StructureIsReused.SafeModules); + it('should reuse the old ts program completely if nothing changed', () => { + testSupport.writeFiles({'src/index.ts': createModuleAndCompSource('main')}); + // Note: the second compile drops factories for library files, + // and therefore changes the structure again + const p1 = compile(); + const p2 = compile(p1); + const p3 = compile(p2); + expect(tsStructureIsReused(p2.getTsProgram())).toBe(StructureIsReused.Completely); + }); + + it('should reuse the old ts program completely if a template or a ts file changed', () => { + testSupport.writeFiles({ + 'src/main.ts': createModuleAndCompSource('main', 'main.html'), + 'src/main.html': `Some template`, + 'src/util.ts': `export const x = 1`, + 'src/index.ts': ` + export * from './main'; + export * from './util'; + ` + }); + // Note: the second compile drops factories for library files, + // and therefore changes the structure again + const p1 = compile(); + const p2 = compile(p1); + testSupport.writeFiles({ + 'src/main.html': `Another template`, + 'src/util.ts': `export const x = 2`, + }); + const p3 = compile(p2); + expect(tsStructureIsReused(p2.getTsProgram())).toBe(StructureIsReused.Completely); + }); + + it('should not reuse the old ts program if an import changed', () => { + testSupport.writeFiles({ + 'src/main.ts': createModuleAndCompSource('main'), + 'src/util.ts': `export const x = 1`, + 'src/index.ts': ` + export * from './main'; + export * from './util'; + ` + }); + // Note: the second compile drops factories for library files, + // and therefore changes the structure again + const p1 = compile(); + const p2 = compile(p1); + testSupport.writeFiles( + {'src/util.ts': `import {Injectable} from '@angular/core'; export const x = 1;`}); + const p3 = compile(p2); + expect(tsStructureIsReused(p2.getTsProgram())).toBe(StructureIsReused.SafeModules); }); }); it('should typecheck templates even if skipTemplateCodegen is set', () => { - writeFiles({ - 'src/main.ts': ` - import {NgModule, Component} from '@angular/core'; - - @Component({selector: 'mycomp', template: '{{nonExistent}}'}) - export class MyComp {} - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - ` + testSupport.writeFiles({ + 'src/main.ts': createModuleAndCompSource('main', `{{nonExistent}}`), }); - const options = createCompilerOptions({skipTemplateCodegen: true}); + const options = testSupport.createCompilerOptions({skipTemplateCodegen: true}); const host = ng.createCompilerHost({options}); - const program = - ng.createProgram({rootNames: [path.resolve(basePath, 'src/main.ts')], options, host}); + const program = ng.createProgram( + {rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host}); const diags = program.getNgSemanticDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText).toBe(`Property 'nonExistent' does not exist on type 'MyComp'.`); + expect(diags[0].messageText).toBe(`Property 'nonExistent' does not exist on type 'mainComp'.`); }); it('should be able to use asynchronously loaded resources', (done) => { - writeFiles({ - 'src/main.ts': ` - import {NgModule, Component} from '@angular/core'; - - @Component({selector: 'mycomp', templateUrl: './main.html'}) - export class MyComp {} - - @NgModule({declarations: [MyComp]}) - export class MyModule {} - `, + testSupport.writeFiles({ + 'src/main.ts': createModuleAndCompSource('main', 'main.html'), // Note: we need to be able to resolve the template synchronously, // only the content is delivered asynchronously. 'src/main.html': '', }); - const options = createCompilerOptions(); + const options = testSupport.createCompilerOptions(); const host = ng.createCompilerHost({options}); host.readResource = () => Promise.resolve('Hello world!'); - const program = - ng.createProgram({rootNames: [path.resolve(basePath, 'src/main.ts')], options, host}); + const program = ng.createProgram( + {rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host}); program.loadNgStructureAsync().then(() => { program.emit(); - const factory = fs.readFileSync(path.resolve(basePath, 'built/src/main.ngfactory.js')); + const factory = + fs.readFileSync(path.resolve(testSupport.basePath, 'built/src/main.ngfactory.js')); expect(factory).toContain('Hello world!'); done(); }); }); }); - -function appComponentSource(): string { - return ` - import {Component, Pipe, Directive} from '@angular/core'; - - export interface Person { - name: string; - address: Address; - } - - export interface Address { - street: string; - city: string; - state: string; - zip: string; - } - - @Component({ - templateUrl: './app.component.html' - }) - export class AppComponent { - name = 'Angular'; - person: Person; - people: Person[]; - maybePerson?: Person; - - getName(): string { return this.name; } - getPerson(): Person { return this.person; } - getMaybePerson(): Person | undefined { return this.maybePerson; } - } - - @Pipe({ - name: 'aPipe', - }) - export class APipe { - transform(n: number): number { return n + 1; } - } - - @Directive({ - selector: '[aDir]', - exportAs: 'aDir' - }) - export class ADirective { - name = 'ADirective'; - } - `; -} - -const QUICKSTART = { - 'src/app.component.ts': appComponentSource(), - 'src/app.component.html': '

Hello {{name}}

', - 'src/app.module.ts': ` - import { NgModule } from '@angular/core'; - import { AppComponent, APipe, ADirective } from './app.component'; - - @NgModule({ - declarations: [ AppComponent, APipe, ADirective ], - bootstrap: [ AppComponent ] - }) - export class AppModule { } - ` -}; - -const LOWERING_QUICKSTART = { - 'src/app.component.ts': appComponentSource(), - 'src/app.component.html': '

Hello {{name}}

', - 'src/app.module.ts': ` - import { NgModule, Component } from '@angular/core'; - - import { AppComponent, APipe, ADirective } from './app.component'; - - class Foo {} - - @Component({ - template: '', - providers: [ - {provide: 'someToken', useFactory: () => new Foo()} - ] - }) - export class Bar {} - - @NgModule({ - declarations: [ AppComponent, APipe, ADirective, Bar ], - bootstrap: [ AppComponent ] - }) - export class AppModule { } - ` -}; - -function expectNoDiagnostics(diagnostics: ng.Diagnostics) { - if (diagnostics && diagnostics.length) { - throw new Error(ng.formatDiagnostics({}, diagnostics)); - } -}