2017-09-19 14:43:34 -04:00
|
|
|
/**
|
|
|
|
* @license
|
2020-05-19 15:08:49 -04:00
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2017-09-19 14:43:34 -04:00
|
|
|
*
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
|
2019-11-02 19:33:31 -04:00
|
|
|
import {ivyEnabled} from '@angular/private/testing';
|
2017-09-19 14:43:34 -04:00
|
|
|
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';
|
|
|
|
|
2020-04-07 15:43:43 -04:00
|
|
|
import {expectNoDiagnostics, setup, TestSupport} from './test_support';
|
2017-09-19 14:43:34 -04:00
|
|
|
|
|
|
|
describe('perform watch', () => {
|
|
|
|
let testSupport: TestSupport;
|
|
|
|
let outDir: string;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
testSupport = setup();
|
|
|
|
outDir = path.resolve(testSupport.basePath, 'outDir');
|
|
|
|
});
|
|
|
|
|
2019-11-02 19:33:31 -04:00
|
|
|
function createConfig(overrideOptions: ng.CompilerOptions = {}): ng.ParsedConfiguration {
|
|
|
|
const options = testSupport.createCompilerOptions({outDir, ...overrideOptions});
|
2017-09-19 14:43:34 -04:00
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
2019-11-02 19:33:31 -04:00
|
|
|
it('should recompile components when its template changes', () => {
|
|
|
|
const config = createConfig({enableIvy: ivyEnabled});
|
|
|
|
const host = new MockWatchHost(config);
|
|
|
|
|
|
|
|
testSupport.writeFiles({
|
|
|
|
'src/main.ts': createModuleAndCompSource('main', './main.html'),
|
|
|
|
'src/main.html': 'initial',
|
|
|
|
'src/index.ts': `export * from './main'; `,
|
|
|
|
});
|
|
|
|
|
|
|
|
const watchResult = performWatchCompilation(host);
|
|
|
|
expectNoDiagnostics(config.options, watchResult.firstCompileResult);
|
|
|
|
|
2019-11-23 15:06:37 -05:00
|
|
|
const htmlPath = path.join(testSupport.basePath, 'src', 'main.html');
|
2019-11-02 19:33:31 -04:00
|
|
|
const genPath = ivyEnabled ? path.posix.join(outDir, 'src', 'main.js') :
|
|
|
|
path.posix.join(outDir, 'src', 'main.ngfactory.js');
|
|
|
|
|
|
|
|
const initial = fs.readFileSync(genPath, {encoding: 'utf8'});
|
|
|
|
expect(initial).toContain('"initial"');
|
|
|
|
|
|
|
|
testSupport.write(htmlPath, 'updated');
|
|
|
|
host.triggerFileChange(FileChangeEvent.Change, htmlPath);
|
|
|
|
|
|
|
|
const updated = fs.readFileSync(genPath, {encoding: 'utf8'});
|
|
|
|
expect(updated).toContain('"updated"');
|
|
|
|
});
|
|
|
|
|
2017-09-19 14:43:34 -04:00
|
|
|
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';
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
2019-01-25 13:50:08 -05:00
|
|
|
const mainTsPath = path.posix.join(testSupport.basePath, 'src', 'main.ts');
|
|
|
|
const utilTsPath = path.posix.join(testSupport.basePath, 'src', 'util.ts');
|
|
|
|
const mainNgFactory = path.posix.join(outDir, 'src', 'main.ngfactory.js');
|
|
|
|
|
2017-09-19 14:43:34 -04:00
|
|
|
performWatchCompilation(host);
|
|
|
|
expect(fs.existsSync(mainNgFactory)).toBe(true);
|
2020-04-07 15:43:43 -04:00
|
|
|
expect(fileExistsSpy!).toHaveBeenCalledWith(mainTsPath);
|
|
|
|
expect(fileExistsSpy!).toHaveBeenCalledWith(utilTsPath);
|
|
|
|
expect(getSourceFileSpy!).toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5);
|
|
|
|
expect(getSourceFileSpy!).toHaveBeenCalledWith(utilTsPath, ts.ScriptTarget.ES5);
|
2017-09-19 14:43:34 -04:00
|
|
|
|
2020-04-07 15:43:43 -04:00
|
|
|
fileExistsSpy!.calls.reset();
|
|
|
|
getSourceFileSpy!.calls.reset();
|
2017-09-19 14:43:34 -04:00
|
|
|
|
|
|
|
// trigger a single file change
|
|
|
|
// -> all other files should be cached
|
|
|
|
host.triggerFileChange(FileChangeEvent.Change, utilTsPath);
|
2017-09-29 18:02:11 -04:00
|
|
|
expectNoDiagnostics(config.options, host.diagnostics);
|
2017-09-19 14:43:34 -04:00
|
|
|
|
2020-04-07 15:43:43 -04:00
|
|
|
expect(fileExistsSpy!).not.toHaveBeenCalledWith(mainTsPath);
|
|
|
|
expect(fileExistsSpy!).toHaveBeenCalledWith(utilTsPath);
|
|
|
|
expect(getSourceFileSpy!).not.toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5);
|
|
|
|
expect(getSourceFileSpy!).toHaveBeenCalledWith(utilTsPath, ts.ScriptTarget.ES5);
|
2017-09-19 14:43:34 -04:00
|
|
|
|
|
|
|
// trigger a folder change
|
|
|
|
// -> nothing should be cached
|
|
|
|
host.triggerFileChange(
|
|
|
|
FileChangeEvent.CreateDeleteDir, path.resolve(testSupport.basePath, 'src'));
|
2017-09-29 18:02:11 -04:00
|
|
|
expectNoDiagnostics(config.options, host.diagnostics);
|
2017-09-19 14:43:34 -04:00
|
|
|
|
2020-04-07 15:43:43 -04:00
|
|
|
expect(fileExistsSpy!).toHaveBeenCalledWith(mainTsPath);
|
|
|
|
expect(fileExistsSpy!).toHaveBeenCalledWith(utilTsPath);
|
|
|
|
expect(getSourceFileSpy!).toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5);
|
|
|
|
expect(getSourceFileSpy!).toHaveBeenCalledWith(utilTsPath, ts.ScriptTarget.ES5);
|
2017-09-19 14:43:34 -04:00
|
|
|
});
|
2017-10-26 18:24:54 -04:00
|
|
|
|
2019-07-08 10:50:19 -04:00
|
|
|
// https://github.com/angular/angular/pull/26036
|
|
|
|
it('should handle redirected source files', () => {
|
|
|
|
const config = createConfig();
|
|
|
|
const host = new MockWatchHost(config);
|
|
|
|
host.createCompilerHost = (options: ng.CompilerOptions) => {
|
|
|
|
const ngHost = ng.createCompilerHost({options});
|
|
|
|
return ngHost;
|
|
|
|
};
|
|
|
|
|
|
|
|
// This file structure has an identical version of "a" under the root node_modules and inside
|
|
|
|
// of "b". Because their package.json file indicates it is the exact same version of "a",
|
|
|
|
// TypeScript will transform the source file of "node_modules/b/node_modules/a/index.d.ts"
|
|
|
|
// into a redirect to "node_modules/a/index.d.ts". During watch compilations, we must assure
|
|
|
|
// not to reintroduce "node_modules/b/node_modules/a/index.d.ts" as its redirected source file,
|
|
|
|
// but instead using its original file.
|
|
|
|
testSupport.writeFiles({
|
|
|
|
'node_modules/a/index.js': `export class ServiceA {}`,
|
|
|
|
'node_modules/a/index.d.ts': `export declare class ServiceA {}`,
|
|
|
|
'node_modules/a/package.json': `{"name": "a", "version": "1.0"}`,
|
|
|
|
'node_modules/b/node_modules/a/index.js': `export class ServiceA {}`,
|
|
|
|
'node_modules/b/node_modules/a/index.d.ts': `export declare class ServiceA {}`,
|
|
|
|
'node_modules/b/node_modules/a/package.json': `{"name": "a", "version": "1.0"}`,
|
|
|
|
'node_modules/b/index.js': `export {ServiceA as ServiceB} from 'a';`,
|
|
|
|
'node_modules/b/index.d.ts': `export {ServiceA as ServiceB} from 'a';`,
|
|
|
|
'src/index.ts': `
|
|
|
|
import {ServiceA} from 'a';
|
|
|
|
import {ServiceB} from 'b';
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
|
|
|
const indexTsPath = path.posix.join(testSupport.basePath, 'src', 'index.ts');
|
|
|
|
|
|
|
|
performWatchCompilation(host);
|
|
|
|
|
|
|
|
// Trigger a file change. This recreates the program from the old program. If redirect sources
|
|
|
|
// were introduced into the new program, this would fail due to an assertion failure in TS.
|
|
|
|
host.triggerFileChange(FileChangeEvent.Change, indexTsPath);
|
|
|
|
expectNoDiagnostics(config.options, host.diagnostics);
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2017-10-26 18:24:54 -04:00
|
|
|
it('should recover from static analysis errors', () => {
|
|
|
|
const config = createConfig();
|
|
|
|
const host = new MockWatchHost(config);
|
|
|
|
|
|
|
|
const okFileContent = `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
@NgModule()
|
|
|
|
export class MyModule {}
|
|
|
|
`;
|
|
|
|
const errorFileContent = `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
2017-11-16 01:33:30 -05:00
|
|
|
@NgModule((() => (1===1 ? null as any : null as any)) as any)
|
2017-10-26 18:24:54 -04:00
|
|
|
export class MyModule {}
|
|
|
|
`;
|
|
|
|
const indexTsPath = path.resolve(testSupport.basePath, 'src', 'index.ts');
|
|
|
|
|
|
|
|
testSupport.write(indexTsPath, okFileContent);
|
|
|
|
|
|
|
|
performWatchCompilation(host);
|
|
|
|
expectNoDiagnostics(config.options, host.diagnostics);
|
|
|
|
|
|
|
|
// Do it multiple times as the watch mode switches internal modes.
|
|
|
|
// E.g. from regular compile to using summaries, ...
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
host.diagnostics = [];
|
|
|
|
testSupport.write(indexTsPath, okFileContent);
|
|
|
|
host.triggerFileChange(FileChangeEvent.Change, indexTsPath);
|
|
|
|
expectNoDiagnostics(config.options, host.diagnostics);
|
|
|
|
|
|
|
|
host.diagnostics = [];
|
|
|
|
testSupport.write(indexTsPath, errorFileContent);
|
|
|
|
host.triggerFileChange(FileChangeEvent.Change, indexTsPath);
|
|
|
|
|
|
|
|
const errDiags = host.diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error);
|
|
|
|
expect(errDiags.length).toBe(1);
|
2017-11-14 20:49:47 -05:00
|
|
|
expect(errDiags[0].messageText).toContain('Function expressions are not supported');
|
2017-10-26 18:24:54 -04:00
|
|
|
}
|
|
|
|
});
|
2017-09-19 14:43:34 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
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 {
|
2017-10-26 18:24:54 -04:00
|
|
|
nextTimeoutListenerId = 1;
|
|
|
|
timeoutListeners: {[id: string]: (() => void)} = {};
|
2017-09-19 14:43:34 -04:00
|
|
|
fileChangeListeners: Array<((event: FileChangeEvent, fileName: string) => void)|null> = [];
|
2017-12-22 12:36:47 -05:00
|
|
|
diagnostics: ng.Diagnostic[] = [];
|
2017-09-19 14:43:34 -04:00
|
|
|
constructor(public config: ng.ParsedConfiguration) {}
|
|
|
|
|
2020-04-07 15:43:43 -04:00
|
|
|
reportDiagnostics(diags: ng.Diagnostics) {
|
|
|
|
this.diagnostics.push(...(diags as ng.Diagnostic[]));
|
|
|
|
}
|
|
|
|
readConfiguration() {
|
|
|
|
return this.config;
|
|
|
|
}
|
|
|
|
createCompilerHost(options: ng.CompilerOptions) {
|
|
|
|
return ng.createCompilerHost({options});
|
|
|
|
}
|
|
|
|
createEmitCallback() {
|
|
|
|
return undefined;
|
|
|
|
}
|
2017-09-19 14:43:34 -04:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|
2017-10-26 18:24:54 -04:00
|
|
|
setTimeout(callback: () => void): any {
|
|
|
|
const id = this.nextTimeoutListenerId++;
|
|
|
|
this.timeoutListeners[id] = callback;
|
2017-09-19 14:43:34 -04:00
|
|
|
return id;
|
|
|
|
}
|
2020-04-07 15:43:43 -04:00
|
|
|
clearTimeout(timeoutId: any): void {
|
|
|
|
delete this.timeoutListeners[timeoutId];
|
|
|
|
}
|
2017-09-19 14:43:34 -04:00
|
|
|
flushTimeouts() {
|
2017-10-26 18:24:54 -04:00
|
|
|
const listeners = this.timeoutListeners;
|
|
|
|
this.timeoutListeners = {};
|
|
|
|
Object.keys(listeners).forEach(id => listeners[id]());
|
2017-09-19 14:43:34 -04:00
|
|
|
}
|
|
|
|
triggerFileChange(event: FileChangeEvent, fileName: string) {
|
|
|
|
this.fileChangeListeners.forEach(listener => {
|
|
|
|
if (listener) {
|
|
|
|
listener(event, fileName);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.flushTimeouts();
|
|
|
|
}
|
|
|
|
}
|