perf(compiler): speed up watch mode (#19275)
- 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
This commit is contained in:
@ -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));
|
||||
}
|
||||
}
|
||||
|
165
packages/compiler-cli/test/perform_watch_spec.ts
Normal file
165
packages/compiler-cli/test/perform_watch_spec.ts
Normal file
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
]);
|
||||
}
|
||||
|
@ -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', () => {
|
||||
|
@ -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': '<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));
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user