mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
8d7f1098d8
As outlined in the previous commit which enabled the `esModuleInterop` TypeScript compiler option, we need to update all namespace imports for `typescript` to default imports. This is needed to allow for TypeScript to be imported at runtime from an ES module. Similar changes are needed for modules like `semver` where the types incorrectly suggest named exports that will not exist at runtime when imported from ESM. This commit refactors all imports to match with the lint rule we have configured in the previous commit. See the previous commit for more details on why certain imports have been changed. A special case are the imports to `@babel/core` and `@babel/types`. For these a special interop is needed as both default imports, or named imports break the other module format. e.g default imports would work well for ESM, but it breaks for CJS. For CJS, the named imports would only work, but in ESM, only the default export exist. We work around this for now until the devmode is using ESM as well (which would be consistent with prodmode and gives us more valuable test results). More details on the interop can be found in the `babel_core.ts` files (two interops are needed for both localize/or the compiler-cli). PR Close #43431
286 lines
10 KiB
TypeScript
286 lines
10 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC 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 {ivyEnabled} from '@angular/private/testing';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import ts from 'typescript';
|
|
|
|
import * as ng from '../index';
|
|
import {FileChangeEvent, performWatchCompilation} from '../src/perform_watch';
|
|
|
|
import {expectNoDiagnostics, setup, TestSupport} from './test_support';
|
|
|
|
describe('perform watch', () => {
|
|
let testSupport: TestSupport;
|
|
let outDir: string;
|
|
|
|
beforeEach(() => {
|
|
testSupport = setup();
|
|
outDir = path.resolve(testSupport.basePath, 'outDir');
|
|
});
|
|
|
|
function createConfig(overrideOptions: ng.CompilerOptions = {}): ng.ParsedConfiguration {
|
|
const options = testSupport.createCompilerOptions({outDir, ...overrideOptions});
|
|
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 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);
|
|
|
|
const htmlPath = path.join(testSupport.basePath, 'src', 'main.html');
|
|
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"');
|
|
});
|
|
|
|
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.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');
|
|
|
|
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
|
|
host.triggerFileChange(FileChangeEvent.Change, utilTsPath);
|
|
expectNoDiagnostics(config.options, host.diagnostics);
|
|
|
|
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
|
|
host.triggerFileChange(
|
|
FileChangeEvent.CreateDeleteDir, path.resolve(testSupport.basePath, 'src'));
|
|
expectNoDiagnostics(config.options, host.diagnostics);
|
|
|
|
expect(fileExistsSpy!).toHaveBeenCalledWith(mainTsPath);
|
|
expect(fileExistsSpy!).toHaveBeenCalledWith(utilTsPath);
|
|
expect(getSourceFileSpy!).toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5);
|
|
expect(getSourceFileSpy!).toHaveBeenCalledWith(utilTsPath, ts.ScriptTarget.ES5);
|
|
});
|
|
|
|
// 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);
|
|
});
|
|
|
|
|
|
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';
|
|
|
|
@NgModule((() => (1===1 ? null as any : null as any)) as any)
|
|
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);
|
|
expect(errDiags[0].messageText).toContain('Function expressions are not supported');
|
|
}
|
|
});
|
|
});
|
|
|
|
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 {
|
|
nextTimeoutListenerId = 1;
|
|
timeoutListeners: {[id: string]: (() => void)} = {};
|
|
fileChangeListeners: Array<((event: FileChangeEvent, fileName: string) => void)|null> = [];
|
|
diagnostics: ng.Diagnostic[] = [];
|
|
constructor(public config: ng.ParsedConfiguration) {}
|
|
|
|
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;
|
|
}
|
|
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): any {
|
|
const id = this.nextTimeoutListenerId++;
|
|
this.timeoutListeners[id] = callback;
|
|
return id;
|
|
}
|
|
clearTimeout(timeoutId: any): void {
|
|
delete this.timeoutListeners[timeoutId];
|
|
}
|
|
flushTimeouts() {
|
|
const listeners = this.timeoutListeners;
|
|
this.timeoutListeners = {};
|
|
Object.keys(listeners).forEach(id => listeners[id]());
|
|
}
|
|
triggerFileChange(event: FileChangeEvent, fileName: string) {
|
|
this.fileChangeListeners.forEach(listener => {
|
|
if (listener) {
|
|
listener(event, fileName);
|
|
}
|
|
});
|
|
this.flushTimeouts();
|
|
}
|
|
}
|