402 lines
16 KiB
TypeScript
402 lines
16 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 * as compiler from '@angular/compiler';
|
|
import * as ts from 'typescript';
|
|
|
|
import {MetadataCollector} from '../../src/metadata/collector';
|
|
import {CompilerHost, CompilerOptions, LibrarySummary} from '../../src/transformers/api';
|
|
import {createCompilerHost, TsCompilerAotCompilerTypeCheckHostAdapter} 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: {generateFile: jasmine.Spy; findGeneratedFileNames: jasmine.Spy;};
|
|
|
|
beforeEach(() => {
|
|
codeGenerator = {
|
|
generateFile: jasmine.createSpy('generateFile').and.returnValue(null),
|
|
findGeneratedFileNames: jasmine.createSpy('findGeneratedFileNames').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,
|
|
},
|
|
rootNames = ['/tmp/index.ts'],
|
|
ngHost = createNgHost({files}),
|
|
librarySummaries = [],
|
|
}: {
|
|
files?: Directory,
|
|
options?: CompilerOptions,
|
|
rootNames?: string[],
|
|
ngHost?: CompilerHost,
|
|
librarySummaries?: LibrarySummary[]
|
|
} = {}) {
|
|
return new TsCompilerAotCompilerTypeCheckHostAdapter(
|
|
rootNames, options, ngHost, new MetadataCollector(), codeGenerator,
|
|
new Map(
|
|
librarySummaries.map(entry => [entry.fileName, entry] as [string, LibrarySummary])));
|
|
}
|
|
|
|
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 allow an import o a package whose name contains dot (e.g. @angular.io)', () => {
|
|
expect(host.fileNameToModuleName('/tmp/node_modules/@angular.io/core.d.ts', '/tmp/main.ts'))
|
|
.toBe('@angular.io/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 use a relative import when accessing generated files, even if crossing packages',
|
|
() => {
|
|
expect(host.fileNameToModuleName(
|
|
'/tmp/node_modules/mod2/b.ngfactory.d.ts',
|
|
'/tmp/node_modules/mod1/a.ngfactory.d.ts'))
|
|
.toBe('../mod2/b.ngfactory');
|
|
});
|
|
|
|
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 containing 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');
|
|
});
|
|
|
|
it('should work well with windows paths', () => {
|
|
const host = createHost({
|
|
rootNames: ['\\tmp\\index.ts'],
|
|
options: {basePath: '\\tmp'},
|
|
files: {'tmp': {'node_modules': {'@core': {'index.d.ts': dummyModule}}}}
|
|
});
|
|
expect(host.moduleNameToFileName('@core/index')).toBe('/tmp/node_modules/@core/index.d.ts');
|
|
});
|
|
});
|
|
|
|
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');
|
|
});
|
|
it('should resolve Sass imports to generated .css files', () => {
|
|
const host = createHost({files: {'tmp': {'src': {'a': {'style.css': 'h1: bold'}}}}});
|
|
expect(host.resourceNameToFileName('./a/style.scss', '/tmp/src/index.ts'))
|
|
.toBe('/tmp/src/a/style.css');
|
|
});
|
|
it('should resolve Less imports to generated .css files', () => {
|
|
const host = createHost({files: {'tmp': {'src': {'a': {'style.css': 'h1: bold'}}}}});
|
|
expect(host.resourceNameToFileName('./a/style.less', '/tmp/src/index.ts'))
|
|
.toBe('/tmp/src/a/style.css');
|
|
});
|
|
it('should resolve Stylus imports to generated .css files', () => {
|
|
const host = createHost({files: {'tmp': {'src': {'a': {'style.css': 'h1: bold'}}}}});
|
|
expect(host.resourceNameToFileName('./a/style.styl', '/tmp/src/index.ts'))
|
|
.toBe('/tmp/src/a/style.css');
|
|
});
|
|
});
|
|
|
|
describe('addGeneratedFile', () => {
|
|
function generate(path: string, files: {}) {
|
|
codeGenerator.findGeneratedFileNames.and.returnValue([`${path}.ngfactory.ts`]);
|
|
codeGenerator.generateFile.and.returnValue(
|
|
new compiler.GeneratedFile(`${path}.ts`, `${path}.ngfactory.ts`, []));
|
|
const host = createHost({
|
|
files,
|
|
options: {
|
|
basePath: '/tmp',
|
|
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
|
// Request UMD, which should get default module names
|
|
module: ts.ModuleKind.UMD
|
|
},
|
|
});
|
|
return host.getSourceFile(`${path}.ngfactory.ts`, ts.ScriptTarget.Latest);
|
|
}
|
|
|
|
it('should include a moduleName when the file is in node_modules', () => {
|
|
const genSf = generate(
|
|
'/tmp/node_modules/@angular/core/core',
|
|
{'tmp': {'node_modules': {'@angular': {'core': {'core.ts': `// some content`}}}}});
|
|
expect(genSf.moduleName).toBe('@angular/core/core.ngfactory');
|
|
});
|
|
|
|
it('should not get tripped on nested node_modules', () => {
|
|
const genSf = generate('/tmp/node_modules/lib1/node_modules/lib2/thing', {
|
|
'tmp':
|
|
{'node_modules': {'lib1': {'node_modules': {'lib2': {'thing.ts': `// some content`}}}}}
|
|
});
|
|
expect(genSf.moduleName).toBe('lib2/thing.ngfactory');
|
|
});
|
|
});
|
|
|
|
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.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']);
|
|
codeGenerator.generateFile.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.generateFile).toHaveBeenCalledTimes(1);
|
|
expect(codeGenerator.findGeneratedFileNames).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should generate code when asking for the generated name first', () => {
|
|
codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']);
|
|
codeGenerator.generateFile.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.generateFile).toHaveBeenCalledTimes(1);
|
|
expect(codeGenerator.findGeneratedFileNames).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should clear old generated references if the original host cached them', () => {
|
|
codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']);
|
|
|
|
const sfText = `
|
|
/// <reference path="main.ts"/>
|
|
`;
|
|
const ngHost = createNgHost({files: {'tmp': {'src': {'index.ts': sfText}}}});
|
|
const sf = ts.createSourceFile('/tmp/src/index.ts', sfText, ts.ScriptTarget.Latest);
|
|
ngHost.getSourceFile = () => sf;
|
|
|
|
codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']);
|
|
codeGenerator.generateFile.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.findGeneratedFileNames.and.returnValue([]);
|
|
codeGenerator.generateFile.and.returnValue(null);
|
|
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');
|
|
});
|
|
|
|
it('should generate for tsx files', () => {
|
|
codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']);
|
|
codeGenerator.generateFile.and.returnValue(aGeneratedFile);
|
|
const host = createHost({files: {'tmp': {'src': {'index.tsx': ``}}}});
|
|
|
|
const genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
|
|
expect(genSf.text).toBe(aGeneratedFileText);
|
|
|
|
const sf = host.getSourceFile('/tmp/src/index.tsx', ts.ScriptTarget.Latest);
|
|
expect(sf.referencedFiles[0].fileName).toBe('/tmp/src/index.ngfactory.ts');
|
|
|
|
// the codegen should have been cached
|
|
expect(codeGenerator.generateFile).toHaveBeenCalledTimes(1);
|
|
expect(codeGenerator.findGeneratedFileNames).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('updateSourceFile', () => {
|
|
it('should update source files', () => {
|
|
codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']);
|
|
codeGenerator.generateFile.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.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']);
|
|
codeGenerator.generateFile.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'));
|
|
});
|
|
});
|
|
});
|