
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
289 lines
11 KiB
TypeScript
289 lines
11 KiB
TypeScript
/**
|
|
* @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 compiler from '@angular/compiler';
|
|
import * as ts from 'typescript';
|
|
|
|
import {MetadataCollector} from '../../src/metadata/collector';
|
|
import {CompilerHost, CompilerOptions} from '../../src/transformers/api';
|
|
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', () => {
|
|
let codeGenerator: jasmine.Spy;
|
|
|
|
beforeEach(() => { codeGenerator = jasmine.createSpy('codeGenerator').and.returnValue([]); });
|
|
|
|
function createNgHost({files = {}}: {files?: Directory} = {}): CompilerHost {
|
|
const context = new MockAotContext('/tmp/', files);
|
|
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 host: TsCompilerAotCompilerTypeCheckHostAdapter;
|
|
beforeEach(() => { host = createHost(); });
|
|
|
|
it('should use a package import when accessing a package from a source file', () => {
|
|
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(host.fileNameToModuleName(
|
|
'/tmp/node_modules/mod1/index.d.ts', '/tmp/node_modules/mod2/index.d.ts'))
|
|
.toBe('mod1/index');
|
|
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(host.fileNameToModuleName(
|
|
'/tmp/node_modules/mod/a/child.d.ts', '/tmp/node_modules/mod/index.d.ts'))
|
|
.toBe('./a/child');
|
|
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(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 hostWithMultipleRoots = createHost({
|
|
options: {
|
|
basePath: '/tmp/',
|
|
rootDirs: [
|
|
'src/a',
|
|
'src/b',
|
|
]
|
|
}
|
|
});
|
|
// both files are in the rootDirs
|
|
expect(hostWithMultipleRoots.fileNameToModuleName('/tmp/src/b/b.ts', '/tmp/src/a/a.ts'))
|
|
.toBe('./b');
|
|
|
|
// one file is not in the rootDirs
|
|
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(
|
|
() => 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');
|
|
});
|
|
|
|
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 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 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 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(host.resourceNameToFileName('./a/non-existing.html', '/tmp/src/index.ts')).toBe(null);
|
|
});
|
|
|
|
it('should resolve package paths as relative paths', () => {
|
|
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 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'));
|
|
});
|
|
});
|
|
});
|