perf(compiler): make the creation of ts.Program faster. (#19275)

We now create 2 programs with exactly the same fileNames and
exactly the same `import` / `export` declarations,
allowing TS to reuse the structure of first program
completely. When passing in an oldProgram and the files didn’t change,
TS can also reuse the old program completely.

This is possible buy adding generated files to TS
in `host.geSourceFile` via `ts.SourceFile.referencedFiles`.

This commit also:
- has a minor side effect on how we generate shared stylesheets:
  - previously every import in a stylesheet would generate a new
    `.ngstyles.ts` file.
  - now, we only generate 1 `.ngstyles.ts` file per entry in `@Component.styleUrls`.
  This was required as we need to be able to determine the program files
  without loading the resources (which can be async).
- makes all angular related methods in `CompilerHost`
  optional, allowing to just use a regular `ts.CompilerHost` as `CompilerHost`.
- simplifies the logic around `Compiler.analyzeNgModules` by introducing `NgAnalyzedFile`.

Perf impact: 1.5s improvement in compiling angular io
PR Close #19275
This commit is contained in:
Tobias Bosch
2017-09-12 09:40:28 -07:00
committed by Igor Minar
parent 9d2236a4b5
commit edd5f5a333
35 changed files with 1929 additions and 1536 deletions
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotCompilerHost, AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, InterpolationConfig, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, Parser, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, TemplateParser, analyzeNgModules, createOfflineCompileUrlResolver, extractProgramSymbols} from '@angular/compiler';
import {AotCompilerHost, AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, InterpolationConfig, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, Parser, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, TemplateParser, analyzeNgModules, createOfflineCompileUrlResolver} from '@angular/compiler';
import {ViewEncapsulation, ɵConsole as Console} from '@angular/core';
import {CompilerHostContext} from 'compiler-cli';
import * as fs from 'fs';
@@ -175,9 +175,10 @@ export class DiagnosticContext {
new DirectiveNormalizer(resourceLoader, urlResolver, htmlParser, config);
result = this._resolver = new CompileMetadataResolver(
config, moduleResolver, directiveResolver, pipeResolver, new JitSummaryResolver(),
elementSchemaRegistry, directiveNormalizer, new Console(), staticSymbolCache,
this.reflector, (error, type) => this.collectError(error, type && type.filePath));
config, htmlParser, moduleResolver, directiveResolver, pipeResolver,
new JitSummaryResolver(), elementSchemaRegistry, directiveNormalizer, new Console(),
staticSymbolCache, this.reflector,
(error, type) => this.collectError(error, type && type.filePath));
}
return result;
}
@@ -186,12 +187,9 @@ export class DiagnosticContext {
let analyzedModules = this._analyzedModules;
if (!analyzedModules) {
const analyzeHost = {isSourceFile(filePath: string) { return true; }};
const programSymbols = extractProgramSymbols(
this.staticSymbolResolver, this.program.getSourceFiles().map(sf => sf.fileName),
analyzeHost);
const programFiles = this.program.getSourceFiles().map(sf => sf.fileName);
analyzedModules = this._analyzedModules =
analyzeNgModules(programSymbols, analyzeHost, this.staticSymbolResolver, this.resolver);
analyzeNgModules(programFiles, analyzeHost, this.staticSymbolResolver, this.resolver);
}
return analyzedModules;
}
+1 -1
View File
@@ -111,7 +111,7 @@ export class MockCompilerHost implements ts.CompilerHost {
fileName: string, languageVersion: ts.ScriptTarget,
onError?: (message: string) => void): ts.SourceFile {
const sourceText = this.context.readFile(fileName);
if (sourceText) {
if (sourceText != null) {
return ts.createSourceFile(fileName, sourceText, languageVersion);
} else {
return undefined !;
+2 -7
View File
@@ -363,7 +363,6 @@ describe('ngc transformer command-line', () => {
writeConfig(`{
"extends": "./tsconfig-base.json",
"angularCompilerOptions": {
"enableSummariesForJit": true
},
"include": ["src/**/*.ts"]
}`);
@@ -379,7 +378,6 @@ describe('ngc transformer command-line', () => {
writeConfig(`{
"extends": "./tsconfig-base.json",
"angularCompilerOptions": {
"enableSummariesForJit": true,
"skipTemplateCodegen": true
},
"compilerOptions": {
@@ -410,7 +408,6 @@ describe('ngc transformer command-line', () => {
writeConfig(`{
"extends": "./tsconfig-base.json",
"angularCompilerOptions": {
"enableSummariesForJit": true,
"skipTemplateCodegen": false
},
"compilerOptions": {
@@ -826,8 +823,7 @@ describe('ngc transformer command-line', () => {
write('lib1/tsconfig-lib1.json', `{
"extends": "../tsconfig-base.json",
"angularCompilerOptions": {
"generateCodeForLibraries": false,
"enableSummariesForJit": true
"generateCodeForLibraries": false
},
"compilerOptions": {
"rootDir": ".",
@@ -849,8 +845,7 @@ describe('ngc transformer command-line', () => {
write('lib2/tsconfig-lib2.json', `{
"extends": "../tsconfig-base.json",
"angularCompilerOptions": {
"generateCodeForLibraries": false,
"enableSummariesForJit": true
"generateCodeForLibraries": false
},
"compilerOptions": {
"rootDir": ".",
@@ -6,59 +6,78 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as compiler from '@angular/compiler';
import * as ts from 'typescript';
import {MetadataCollector} from '../../src/metadata/collector';
import {CompilerHost, CompilerOptions} from '../../src/transformers/api';
import {createCompilerHost} from '../../src/transformers/compiler_host';
import {TsCompilerAotCompilerTypeCheckHostAdapter, createCompilerHost} from '../../src/transformers/compiler_host';
import {Directory, Entry, MockAotContext, MockCompilerHost} from '../mocks';
const dummyModule = 'export let foo: any[];';
const aGeneratedFile = new compiler.GeneratedFile(
'/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts',
[new compiler.DeclareVarStmt('x', new compiler.LiteralExpr(1))]);
const aGeneratedFileText = `var x:any = 1;\n`;
describe('NgCompilerHost', () => {
function createHost(
{files = {}, options = {basePath: '/tmp'}}: {files?: Directory,
options?: CompilerOptions} = {}) {
let codeGenerator: jasmine.Spy;
beforeEach(() => { codeGenerator = jasmine.createSpy('codeGenerator').and.returnValue([]); });
function createNgHost({files = {}}: {files?: Directory} = {}): CompilerHost {
const context = new MockAotContext('/tmp/', files);
const tsHost = new MockCompilerHost(context);
return createCompilerHost({tsHost, options});
return new MockCompilerHost(context) as ts.CompilerHost;
}
function createHost({
files = {},
options = {
basePath: '/tmp',
moduleResolution: ts.ModuleResolutionKind.NodeJs,
},
ngHost = createNgHost({files}),
}: {files?: Directory, options?: CompilerOptions, ngHost?: CompilerHost} = {}) {
return new TsCompilerAotCompilerTypeCheckHostAdapter(
['/tmp/index.ts'], options, ngHost, new MetadataCollector(), codeGenerator);
}
describe('fileNameToModuleName', () => {
let ngHost: CompilerHost;
beforeEach(() => { ngHost = createHost(); });
let host: TsCompilerAotCompilerTypeCheckHostAdapter;
beforeEach(() => { host = createHost(); });
it('should use a package import when accessing a package from a source file', () => {
expect(ngHost.fileNameToModuleName('/tmp/node_modules/@angular/core.d.ts', '/tmp/main.ts'))
expect(host.fileNameToModuleName('/tmp/node_modules/@angular/core.d.ts', '/tmp/main.ts'))
.toBe('@angular/core');
});
it('should use a package import when accessing a package from another package', () => {
expect(ngHost.fileNameToModuleName(
expect(host.fileNameToModuleName(
'/tmp/node_modules/mod1/index.d.ts', '/tmp/node_modules/mod2/index.d.ts'))
.toBe('mod1/index');
expect(ngHost.fileNameToModuleName(
expect(host.fileNameToModuleName(
'/tmp/node_modules/@angular/core/index.d.ts',
'/tmp/node_modules/@angular/common/index.d.ts'))
.toBe('@angular/core/index');
});
it('should use a relative import when accessing a file in the same package', () => {
expect(ngHost.fileNameToModuleName(
expect(host.fileNameToModuleName(
'/tmp/node_modules/mod/a/child.d.ts', '/tmp/node_modules/mod/index.d.ts'))
.toBe('./a/child');
expect(ngHost.fileNameToModuleName(
expect(host.fileNameToModuleName(
'/tmp/node_modules/@angular/core/src/core.d.ts',
'/tmp/node_modules/@angular/core/index.d.ts'))
.toBe('./src/core');
});
it('should use a relative import when accessing a source file from a source file', () => {
expect(ngHost.fileNameToModuleName('/tmp/src/a/child.ts', '/tmp/src/index.ts'))
expect(host.fileNameToModuleName('/tmp/src/a/child.ts', '/tmp/src/index.ts'))
.toBe('./a/child');
});
it('should support multiple rootDirs when accessing a source file form a source file', () => {
const ngHostWithMultipleRoots = createHost({
const hostWithMultipleRoots = createHost({
options: {
basePath: '/tmp/',
rootDirs: [
@@ -68,59 +87,202 @@ describe('NgCompilerHost', () => {
}
});
// both files are in the rootDirs
expect(ngHostWithMultipleRoots.fileNameToModuleName('/tmp/src/b/b.ts', '/tmp/src/a/a.ts'))
expect(hostWithMultipleRoots.fileNameToModuleName('/tmp/src/b/b.ts', '/tmp/src/a/a.ts'))
.toBe('./b');
// one file is not in the rootDirs
expect(ngHostWithMultipleRoots.fileNameToModuleName('/tmp/src/c/c.ts', '/tmp/src/a/a.ts'))
expect(hostWithMultipleRoots.fileNameToModuleName('/tmp/src/c/c.ts', '/tmp/src/a/a.ts'))
.toBe('../c/c');
});
it('should error if accessing a source file from a package', () => {
expect(
() => ngHost.fileNameToModuleName(
() => host.fileNameToModuleName(
'/tmp/src/a/child.ts', '/tmp/node_modules/@angular/core.d.ts'))
.toThrowError(
'Trying to import a source file from a node_modules package: import /tmp/src/a/child.ts from /tmp/node_modules/@angular/core.d.ts');
'Trying to import a source file from a node_modules package: ' +
'import /tmp/src/a/child.ts from /tmp/node_modules/@angular/core.d.ts');
});
it('should use the provided implementation if any', () => {
const ngHost = createNgHost();
ngHost.fileNameToModuleName = () => 'someResult';
const host = createHost({ngHost});
expect(host.fileNameToModuleName('a', 'b')).toBe('someResult');
});
});
describe('moduleNameToFileName', () => {
it('should resolve a package import without a containing file', () => {
const ngHost = createHost(
{files: {'tmp': {'node_modules': {'@angular': {'core': {'index.d.ts': dummyModule}}}}}});
expect(ngHost.moduleNameToFileName('@angular/core'))
.toBe('/tmp/node_modules/@angular/core/index.d.ts');
it('should resolve an import using the containing file', () => {
const host = createHost({files: {'tmp': {'src': {'a': {'child.d.ts': dummyModule}}}}});
expect(host.moduleNameToFileName('./a/child', '/tmp/src/index.ts'))
.toBe('/tmp/src/a/child.d.ts');
});
it('should resolve an import using the containing file', () => {
const ngHost = createHost({files: {'tmp': {'src': {'a': {'child.d.ts': dummyModule}}}}});
expect(ngHost.moduleNameToFileName('./a/child', '/tmp/src/index.ts'))
.toBe('/tmp/src/a/child.d.ts');
it('should allow to skip the containg file for package imports', () => {
const host =
createHost({files: {'tmp': {'node_modules': {'@core': {'index.d.ts': dummyModule}}}}});
expect(host.moduleNameToFileName('@core/index')).toBe('/tmp/node_modules/@core/index.d.ts');
});
it('should use the provided implementation if any', () => {
const ngHost = createNgHost();
ngHost.moduleNameToFileName = () => 'someResult';
const host = createHost({ngHost});
expect(host.moduleNameToFileName('a', 'b')).toBe('someResult');
});
});
describe('resourceNameToFileName', () => {
it('should resolve a relative import', () => {
const ngHost = createHost({files: {'tmp': {'src': {'a': {'child.html': '<div>'}}}}});
expect(ngHost.resourceNameToFileName('./a/child.html', '/tmp/src/index.ts'))
const host = createHost({files: {'tmp': {'src': {'a': {'child.html': '<div>'}}}}});
expect(host.resourceNameToFileName('./a/child.html', '/tmp/src/index.ts'))
.toBe('/tmp/src/a/child.html');
expect(ngHost.resourceNameToFileName('./a/non-existing.html', '/tmp/src/index.ts'))
.toBe(null);
expect(host.resourceNameToFileName('./a/non-existing.html', '/tmp/src/index.ts')).toBe(null);
});
it('should resolve package paths as relative paths', () => {
const ngHost = createHost({files: {'tmp': {'src': {'a': {'child.html': '<div>'}}}}});
expect(ngHost.resourceNameToFileName('a/child.html', '/tmp/src/index.ts'))
const host = createHost({files: {'tmp': {'src': {'a': {'child.html': '<div>'}}}}});
expect(host.resourceNameToFileName('a/child.html', '/tmp/src/index.ts'))
.toBe('/tmp/src/a/child.html');
});
it('should resolve absolute paths as package paths', () => {
const ngHost = createHost({files: {'tmp': {'node_modules': {'a': {'child.html': '<div>'}}}}});
expect(ngHost.resourceNameToFileName('/a/child.html', ''))
const host = createHost({files: {'tmp': {'node_modules': {'a': {'child.html': '<div>'}}}}});
expect(host.resourceNameToFileName('/a/child.html', ''))
.toBe('/tmp/node_modules/a/child.html');
});
it('should use the provided implementation if any', () => {
const ngHost = createNgHost();
ngHost.resourceNameToFileName = () => 'someResult';
const host = createHost({ngHost});
expect(host.resourceNameToFileName('a', 'b')).toBe('someResult');
});
});
describe('getSourceFile', () => {
it('should cache source files by name', () => {
const host = createHost({files: {'tmp': {'src': {'index.ts': ``}}}});
const sf1 = host.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
const sf2 = host.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
expect(sf1).toBe(sf2);
});
it('should generate code when asking for the base name and add it as referencedFiles', () => {
codeGenerator.and.returnValue([aGeneratedFile]);
const host = createHost({
files: {
'tmp': {
'src': {
'index.ts': `
/// <reference path="main.ts"/>
`
}
}
}
});
const sf = host.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
expect(sf.referencedFiles[0].fileName).toBe('main.ts');
expect(sf.referencedFiles[1].fileName).toBe('/tmp/src/index.ngfactory.ts');
const genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
expect(genSf.text).toBe(aGeneratedFileText);
// the codegen should have been cached
expect(codeGenerator).toHaveBeenCalledTimes(1);
});
it('should generate code when asking for the generated name first', () => {
codeGenerator.and.returnValue([aGeneratedFile]);
const host = createHost({
files: {
'tmp': {
'src': {
'index.ts': `
/// <reference path="main.ts"/>
`
}
}
}
});
const genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
expect(genSf.text).toBe(aGeneratedFileText);
const sf = host.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
expect(sf.referencedFiles[0].fileName).toBe('main.ts');
expect(sf.referencedFiles[1].fileName).toBe('/tmp/src/index.ngfactory.ts');
// the codegen should have been cached
expect(codeGenerator).toHaveBeenCalledTimes(1);
});
it('should clear old generated references if the original host cached them', () => {
const ngHost = createNgHost();
const sfText = `
/// <reference path="main.ts"/>
`;
const sf = ts.createSourceFile('/tmp/src/index.ts', sfText, ts.ScriptTarget.Latest);
ngHost.getSourceFile = () => sf;
codeGenerator.and.returnValue(
[new compiler.GeneratedFile('/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts', [])]);
const host1 = createHost({ngHost});
host1.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
expect(sf.referencedFiles.length).toBe(2);
expect(sf.referencedFiles[0].fileName).toBe('main.ts');
expect(sf.referencedFiles[1].fileName).toBe('/tmp/src/index.ngfactory.ts');
codeGenerator.and.returnValue([]);
const host2 = createHost({ngHost});
host2.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
expect(sf.referencedFiles.length).toBe(1);
expect(sf.referencedFiles[0].fileName).toBe('main.ts');
});
});
describe('updateSourceFile', () => {
it('should update source files', () => {
codeGenerator.and.returnValue([aGeneratedFile]);
const host = createHost({files: {'tmp': {'src': {'index.ts': ''}}}});
let genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
expect(genSf.text).toBe(aGeneratedFileText);
host.updateGeneratedFile(new compiler.GeneratedFile(
'/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts',
[new compiler.DeclareVarStmt('x', new compiler.LiteralExpr(2))]));
genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
expect(genSf.text).toBe(`var x:any = 2;\n`);
});
it('should error if the imports changed', () => {
codeGenerator.and.returnValue(
[new compiler.GeneratedFile('/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts', [
new compiler.DeclareVarStmt(
'x', new compiler.ExternalExpr(new compiler.ExternalReference('aModule', 'aName')))
])]);
const host = createHost({files: {'tmp': {'src': {'index.ts': ''}}}});
host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
expect(
() => host.updateGeneratedFile(new compiler.GeneratedFile(
'/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts',
[new compiler.DeclareVarStmt(
'x', new compiler.ExternalExpr(
new compiler.ExternalReference('otherModule', 'aName')))])))
.toThrowError([
`Illegal State: external references changed in /tmp/src/index.ngfactory.ts.`,
`Old: aModule.`, `New: otherModule`
].join('\n'));
});
});
});
@@ -0,0 +1,294 @@
/**
* @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 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);
}
describe('ng program', () => {
let basePath: string;
let write: (fileName: string, content: string) => void;
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'));
});
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';
@Component({selector: 'comp', templateUrl: './main.html'})
export class MyComp {}
@NgModule()
export class MyModule {}
`,
'src/main.html': `Hello world`,
};
function expectResuse(newFiles: {[fileName: string]: string}, reuseLevel: StructureIsReused) {
writeFiles(files);
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);
}
it('should reuse completely if nothing changed',
() => { expectResuse({}, StructureIsReused.Completely); });
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);
});
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 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 {}
`
});
const options = createCompilerOptions({skipTemplateCodegen: true});
const host = ng.createCompilerHost({options});
const program =
ng.createProgram({rootNames: [path.resolve(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'.`);
});
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 {}
`,
// Note: we need to be able to resolve the template synchronously,
// only the content is delivered asynchronously.
'src/main.html': '',
});
const options = 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});
program.loadNgStructureAsync().then(() => {
program.emit();
const factory = fs.readFileSync(path.resolve(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': '<h1>Hello {{name}}</h1>',
'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': '<h1>Hello {{name}}</h1>',
'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));
}
}