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
1141 lines
37 KiB
TypeScript
1141 lines
37 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 fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as ts from 'typescript';
|
|
|
|
import {main, readCommandLineAndConfiguration, watchMode} from '../src/main';
|
|
import {makeTempDir} from './test_support';
|
|
|
|
function getNgRootDir() {
|
|
const moduleFilename = module.filename.replace(/\\/g, '/');
|
|
const distIndex = moduleFilename.indexOf('/dist/all');
|
|
return moduleFilename.substr(0, distIndex);
|
|
}
|
|
|
|
describe('ngc transformer command-line', () => {
|
|
let basePath: string;
|
|
let outDir: string;
|
|
let write: (fileName: string, content: string) => void;
|
|
let errorSpy: jasmine.Spy&((s: string) => void);
|
|
|
|
function shouldExist(fileName: string) {
|
|
if (!fs.existsSync(path.resolve(outDir, fileName))) {
|
|
throw new Error(`Expected ${fileName} to be emitted (outDir: ${outDir})`);
|
|
}
|
|
}
|
|
|
|
function shouldNotExist(fileName: string) {
|
|
if (fs.existsSync(path.resolve(outDir, fileName))) {
|
|
throw new Error(`Did not expect ${fileName} to be emitted (outDir: ${outDir})`);
|
|
}
|
|
}
|
|
|
|
function writeConfig(tsconfig: string = '{"extends": "./tsconfig-base.json"}') {
|
|
write('tsconfig.json', tsconfig);
|
|
}
|
|
|
|
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'});
|
|
};
|
|
write('tsconfig-base.json', `{
|
|
"compilerOptions": {
|
|
"experimentalDecorators": true,
|
|
"skipLibCheck": true,
|
|
"noImplicitAny": true,
|
|
"types": [],
|
|
"outDir": "built",
|
|
"rootDir": ".",
|
|
"baseUrl": ".",
|
|
"declaration": true,
|
|
"target": "es5",
|
|
"module": "es2015",
|
|
"moduleResolution": "node",
|
|
"lib": ["es6", "dom"],
|
|
"typeRoots": ["node_modules/@types"]
|
|
}
|
|
}`);
|
|
outDir = path.resolve(basePath, 'built');
|
|
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'));
|
|
});
|
|
|
|
it('should compile without errors', () => {
|
|
writeConfig();
|
|
write('test.ts', 'export const A = 1;');
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
describe('errors', () => {
|
|
|
|
beforeEach(() => { errorSpy.and.stub(); });
|
|
|
|
it('should not print the stack trace if user input file does not exist', () => {
|
|
writeConfig(`{
|
|
"extends": "./tsconfig-base.json",
|
|
"files": ["test.ts"]
|
|
}`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).toHaveBeenCalledWith(
|
|
`error TS6053: File '` + path.join(basePath, 'test.ts') + `' not found.` +
|
|
'\n');
|
|
expect(exitCode).toEqual(1);
|
|
});
|
|
|
|
it('should not print the stack trace if user input file is malformed', () => {
|
|
writeConfig();
|
|
write('test.ts', 'foo;');
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).toHaveBeenCalledWith(
|
|
`test.ts(1,1): error TS2304: Cannot find name 'foo'.` +
|
|
'\n');
|
|
expect(exitCode).toEqual(1);
|
|
});
|
|
|
|
it('should not print the stack trace if cannot find the imported module', () => {
|
|
writeConfig();
|
|
write('test.ts', `import {MyClass} from './not-exist-deps';`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).toHaveBeenCalledWith(
|
|
`test.ts(1,23): error TS2307: Cannot find module './not-exist-deps'.` +
|
|
'\n');
|
|
expect(exitCode).toEqual(1);
|
|
});
|
|
|
|
it('should not print the stack trace if cannot import', () => {
|
|
writeConfig();
|
|
write('empty-deps.ts', 'export const A = 1;');
|
|
write('test.ts', `import {MyClass} from './empty-deps';`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).toHaveBeenCalledWith(
|
|
`test.ts(1,9): error TS2305: Module '"` + path.join(basePath, 'empty-deps') +
|
|
`"' has no exported member 'MyClass'.` +
|
|
'\n');
|
|
expect(exitCode).toEqual(1);
|
|
});
|
|
|
|
it('should not print the stack trace if type mismatches', () => {
|
|
writeConfig();
|
|
write('empty-deps.ts', 'export const A = "abc";');
|
|
write('test.ts', `
|
|
import {A} from './empty-deps';
|
|
A();
|
|
`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).toHaveBeenCalledWith(
|
|
'test.ts(3,9): error TS2349: Cannot invoke an expression whose type lacks a call signature. ' +
|
|
'Type \'String\' has no compatible call signatures.\n');
|
|
expect(exitCode).toEqual(1);
|
|
});
|
|
|
|
it('should print the stack trace on compiler internal errors', () => {
|
|
write('test.ts', 'export const A = 1;');
|
|
|
|
const exitCode = main(['-p', 'not-exist'], errorSpy);
|
|
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
expect(errorSpy.calls.mostRecent().args[0]).toContain('no such file or directory');
|
|
expect(errorSpy.calls.mostRecent().args[0]).toContain('at Error (native)');
|
|
expect(exitCode).toEqual(2);
|
|
});
|
|
|
|
it('should report errors for ngfactory files that are not referenced by root files', () => {
|
|
writeConfig(`{
|
|
"extends": "./tsconfig-base.json",
|
|
"files": ["mymodule.ts"]
|
|
}`);
|
|
write('mymodule.ts', `
|
|
import {NgModule, Component} from '@angular/core';
|
|
|
|
@Component({template: '{{unknownProp}}'})
|
|
export class MyComp {}
|
|
|
|
@NgModule({declarations: [MyComp]})
|
|
export class MyModule {}
|
|
`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
expect(errorSpy.calls.mostRecent().args[0])
|
|
.toContain('Error at ng://' + path.join(basePath, 'mymodule.ts.MyComp.html'));
|
|
expect(errorSpy.calls.mostRecent().args[0])
|
|
.toContain(`Property 'unknownProp' does not exist on type 'MyComp'`);
|
|
|
|
expect(exitCode).toEqual(1);
|
|
});
|
|
|
|
it('should report errors as coming from the html file, not the factory', () => {
|
|
writeConfig(`{
|
|
"extends": "./tsconfig-base.json",
|
|
"files": ["mymodule.ts"]
|
|
}`);
|
|
write('my.component.ts', `
|
|
import {Component} from '@angular/core';
|
|
@Component({templateUrl: './my.component.html'})
|
|
export class MyComp {}
|
|
`);
|
|
write('my.component.html', `<h1>
|
|
{{unknownProp}}
|
|
</h1>`);
|
|
write('mymodule.ts', `
|
|
import {NgModule} from '@angular/core';
|
|
import {MyComp} from './my.component';
|
|
|
|
@NgModule({declarations: [MyComp]})
|
|
export class MyModule {}
|
|
`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
expect(errorSpy.calls.mostRecent().args[0])
|
|
.toContain('Error at ng://' + path.join(basePath, 'my.component.html(1,5):'));
|
|
expect(errorSpy.calls.mostRecent().args[0])
|
|
.toContain(`Property 'unknownProp' does not exist on type 'MyComp'`);
|
|
|
|
expect(exitCode).toEqual(1);
|
|
});
|
|
});
|
|
|
|
describe('compile ngfactory files', () => {
|
|
|
|
it('should compile ngfactory files that are not referenced by root files', () => {
|
|
writeConfig(`{
|
|
"extends": "./tsconfig-base.json",
|
|
"files": ["mymodule.ts"]
|
|
}`);
|
|
write('mymodule.ts', `
|
|
import {CommonModule} from '@angular/common';
|
|
import {NgModule} from '@angular/core';
|
|
|
|
@NgModule({
|
|
imports: [CommonModule]
|
|
})
|
|
export class MyModule {}
|
|
`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(exitCode).toEqual(0);
|
|
|
|
expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngfactory.js'))).toBe(true);
|
|
expect(fs.existsSync(path.resolve(
|
|
outDir, 'node_modules', '@angular', 'core', 'src',
|
|
'application_module.ngfactory.js')))
|
|
.toBe(true);
|
|
});
|
|
|
|
it('should compile with an explicit tsconfig reference', () => {
|
|
writeConfig(`{
|
|
"extends": "./tsconfig-base.json",
|
|
"files": ["mymodule.ts"]
|
|
}`);
|
|
write('mymodule.ts', `
|
|
import {CommonModule} from '@angular/common';
|
|
import {NgModule} from '@angular/core';
|
|
|
|
@NgModule({
|
|
imports: [CommonModule]
|
|
})
|
|
export class MyModule {}
|
|
`);
|
|
|
|
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
|
expect(exitCode).toEqual(0);
|
|
expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngfactory.js'))).toBe(true);
|
|
expect(fs.existsSync(path.resolve(
|
|
outDir, 'node_modules', '@angular', 'core', 'src',
|
|
'application_module.ngfactory.js')))
|
|
.toBe(true);
|
|
});
|
|
|
|
describe(`emit generated files depending on the source file`, () => {
|
|
const modules = ['comp', 'directive', 'module'];
|
|
beforeEach(() => {
|
|
write('src/comp.ts', `
|
|
import {Component, ViewEncapsulation} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'comp-a',
|
|
template: 'A',
|
|
styleUrls: ['plain.css'],
|
|
encapsulation: ViewEncapsulation.None
|
|
})
|
|
export class CompA {
|
|
}
|
|
|
|
@Component({
|
|
selector: 'comp-b',
|
|
template: 'B',
|
|
styleUrls: ['emulated.css']
|
|
})
|
|
export class CompB {
|
|
}`);
|
|
write('src/plain.css', 'div {}');
|
|
write('src/emulated.css', 'div {}');
|
|
write('src/directive.ts', `
|
|
import {Directive, Input} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[someDir]',
|
|
host: {'[title]': 'someProp'},
|
|
})
|
|
export class SomeDirective {
|
|
@Input() someProp: string;
|
|
}`);
|
|
write('src/module.ts', `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {CompA, CompB} from './comp';
|
|
import {SomeDirective} from './directive';
|
|
|
|
@NgModule({
|
|
declarations: [
|
|
CompA, CompB,
|
|
SomeDirective,
|
|
],
|
|
exports: [
|
|
CompA, CompB,
|
|
SomeDirective,
|
|
],
|
|
})
|
|
export class SomeModule {
|
|
}`);
|
|
});
|
|
|
|
function expectJsDtsMetadataJsonToExist() {
|
|
modules.forEach(moduleName => {
|
|
shouldExist(moduleName + '.js');
|
|
shouldExist(moduleName + '.d.ts');
|
|
shouldExist(moduleName + '.metadata.json');
|
|
});
|
|
}
|
|
|
|
function expectAllGeneratedFilesToExist() {
|
|
modules.forEach(moduleName => {
|
|
if (/module|comp/.test(moduleName)) {
|
|
shouldExist(moduleName + '.ngfactory.js');
|
|
shouldExist(moduleName + '.ngfactory.d.ts');
|
|
shouldExist(moduleName + '.ngsummary.js');
|
|
shouldExist(moduleName + '.ngsummary.d.ts');
|
|
} else {
|
|
shouldNotExist(moduleName + '.ngfactory.js');
|
|
shouldNotExist(moduleName + '.ngfactory.d.ts');
|
|
shouldExist(moduleName + '.ngsummary.js');
|
|
shouldExist(moduleName + '.ngsummary.d.ts');
|
|
}
|
|
shouldExist(moduleName + '.ngsummary.json');
|
|
shouldNotExist(moduleName + '.ngfactory.metadata.json');
|
|
shouldNotExist(moduleName + '.ngsummary.metadata.json');
|
|
});
|
|
shouldExist('plain.css.ngstyle.js');
|
|
shouldExist('plain.css.ngstyle.d.ts');
|
|
shouldExist('emulated.css.shim.ngstyle.js');
|
|
shouldExist('emulated.css.shim.ngstyle.d.ts');
|
|
}
|
|
|
|
it('should emit generated files from sources', () => {
|
|
writeConfig(`{
|
|
"extends": "./tsconfig-base.json",
|
|
"angularCompilerOptions": {
|
|
},
|
|
"include": ["src/**/*.ts"]
|
|
}`);
|
|
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
|
expect(exitCode).toEqual(0);
|
|
outDir = path.resolve(basePath, 'built', 'src');
|
|
expectJsDtsMetadataJsonToExist();
|
|
expectAllGeneratedFilesToExist();
|
|
});
|
|
|
|
it('should emit generated files from libraries', () => {
|
|
// first only generate .d.ts / .js / .metadata.json files
|
|
writeConfig(`{
|
|
"extends": "./tsconfig-base.json",
|
|
"angularCompilerOptions": {
|
|
"skipTemplateCodegen": true
|
|
},
|
|
"compilerOptions": {
|
|
"outDir": "lib"
|
|
},
|
|
"include": ["src/**/*.ts"]
|
|
}`);
|
|
let exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
|
expect(exitCode).toEqual(0);
|
|
outDir = path.resolve(basePath, 'lib', 'src');
|
|
modules.forEach(moduleName => {
|
|
shouldExist(moduleName + '.js');
|
|
shouldExist(moduleName + '.d.ts');
|
|
shouldExist(moduleName + '.metadata.json');
|
|
shouldNotExist(moduleName + '.ngfactory.js');
|
|
shouldNotExist(moduleName + '.ngfactory.d.ts');
|
|
shouldNotExist(moduleName + '.ngsummary.js');
|
|
shouldNotExist(moduleName + '.ngsummary.d.ts');
|
|
shouldNotExist(moduleName + '.ngsummary.json');
|
|
shouldNotExist(moduleName + '.ngfactory.metadata.json');
|
|
shouldNotExist(moduleName + '.ngsummary.metadata.json');
|
|
});
|
|
shouldNotExist('src/plain.css.ngstyle.js');
|
|
shouldNotExist('src/plain.css.ngstyle.d.ts');
|
|
shouldNotExist('src/emulated.css.shim.ngstyle.js');
|
|
shouldNotExist('src/emulated.css.shim.ngstyle.d.ts');
|
|
// Then compile again, using the previous .metadata.json as input.
|
|
writeConfig(`{
|
|
"extends": "./tsconfig-base.json",
|
|
"angularCompilerOptions": {
|
|
"skipTemplateCodegen": false
|
|
},
|
|
"compilerOptions": {
|
|
"outDir": "built"
|
|
},
|
|
"include": ["lib/**/*.d.ts"]
|
|
}`);
|
|
write('lib/src/plain.css', 'div {}');
|
|
write('lib/src/emulated.css', 'div {}');
|
|
exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
|
expect(exitCode).toEqual(0);
|
|
outDir = path.resolve(basePath, 'built', 'lib', 'src');
|
|
expectAllGeneratedFilesToExist();
|
|
});
|
|
});
|
|
|
|
describe('closure', () => {
|
|
it('should not generate closure specific code by default', () => {
|
|
writeConfig(`{
|
|
"extends": "./tsconfig-base.json",
|
|
"files": ["mymodule.ts"]
|
|
}`);
|
|
write('mymodule.ts', `
|
|
import {NgModule, Component} from '@angular/core';
|
|
|
|
@Component({template: ''})
|
|
export class MyComp {}
|
|
|
|
@NgModule({declarations: [MyComp]})
|
|
export class MyModule {}
|
|
`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(exitCode).toEqual(0);
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
expect(mymoduleSource).not.toContain('@fileoverview added by tsickle');
|
|
expect(mymoduleSource).toContain('MyComp.decorators = [');
|
|
});
|
|
|
|
it('should add closure annotations', () => {
|
|
writeConfig(`{
|
|
"extends": "./tsconfig-base.json",
|
|
"angularCompilerOptions": {
|
|
"annotateForClosureCompiler": true
|
|
},
|
|
"files": ["mymodule.ts"]
|
|
}`);
|
|
write('mymodule.ts', `
|
|
import {NgModule, Component} from '@angular/core';
|
|
|
|
@Component({template: ''})
|
|
export class MyComp {
|
|
fn(p: any) {}
|
|
}
|
|
|
|
@NgModule({declarations: [MyComp]})
|
|
export class MyModule {}
|
|
`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(exitCode).toEqual(0);
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
expect(mymoduleSource).toContain('@fileoverview added by tsickle');
|
|
expect(mymoduleSource).toContain('@param {?} p');
|
|
});
|
|
|
|
it('should add metadata as decorators', () => {
|
|
writeConfig(`{
|
|
"extends": "./tsconfig-base.json",
|
|
"angularCompilerOptions": {
|
|
"annotationsAs": "decorators"
|
|
},
|
|
"files": ["mymodule.ts"]
|
|
}`);
|
|
write('mymodule.ts', `
|
|
import {NgModule, Component} from '@angular/core';
|
|
|
|
@Component({template: ''})
|
|
export class MyComp {
|
|
fn(p: any) {}
|
|
}
|
|
|
|
@NgModule({declarations: [MyComp]})
|
|
export class MyModule {}
|
|
`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(exitCode).toEqual(0);
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
expect(mymoduleSource).toContain('MyComp = __decorate([');
|
|
});
|
|
});
|
|
|
|
describe('expression lowering', () => {
|
|
beforeEach(() => {
|
|
writeConfig(`{
|
|
"extends": "./tsconfig-base.json",
|
|
"files": ["mymodule.ts"]
|
|
}`);
|
|
});
|
|
|
|
function compile(): number {
|
|
errorSpy.calls.reset();
|
|
const result = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
return result;
|
|
}
|
|
|
|
it('should be able to lower a lambda expression in a provider', () => {
|
|
write('mymodule.ts', `
|
|
import {CommonModule} from '@angular/common';
|
|
import {NgModule} from '@angular/core';
|
|
|
|
class Foo {}
|
|
|
|
@NgModule({
|
|
imports: [CommonModule],
|
|
providers: [{provide: 'someToken', useFactory: () => new Foo()}]
|
|
})
|
|
export class MyModule {}
|
|
`);
|
|
expect(compile()).toEqual(0);
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
expect(mymoduleSource).toContain('var ɵ0 = function () { return new Foo(); }');
|
|
expect(mymoduleSource).toContain('export { ɵ0');
|
|
|
|
const mymodulefactory = path.resolve(outDir, 'mymodule.ngfactory.js');
|
|
const mymodulefactorySource = fs.readFileSync(mymodulefactory, 'utf8');
|
|
expect(mymodulefactorySource).toContain('"someToken", i1.ɵ0');
|
|
});
|
|
|
|
it('should be able to lower a function expression in a provider', () => {
|
|
write('mymodule.ts', `
|
|
import {CommonModule} from '@angular/common';
|
|
import {NgModule} from '@angular/core';
|
|
|
|
class Foo {}
|
|
|
|
@NgModule({
|
|
imports: [CommonModule],
|
|
providers: [{provide: 'someToken', useFactory: function() {return new Foo();}}]
|
|
})
|
|
export class MyModule {}
|
|
`);
|
|
expect(compile()).toEqual(0);
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
expect(mymoduleSource).toContain('var ɵ0 = function () { return new Foo(); }');
|
|
expect(mymoduleSource).toContain('export { ɵ0');
|
|
|
|
const mymodulefactory = path.resolve(outDir, 'mymodule.ngfactory.js');
|
|
const mymodulefactorySource = fs.readFileSync(mymodulefactory, 'utf8');
|
|
expect(mymodulefactorySource).toContain('"someToken", i1.ɵ0');
|
|
});
|
|
|
|
it('should able to lower multiple expressions', () => {
|
|
write('mymodule.ts', `
|
|
import {CommonModule} from '@angular/common';
|
|
import {NgModule} from '@angular/core';
|
|
|
|
class Foo {}
|
|
|
|
@NgModule({
|
|
imports: [CommonModule],
|
|
providers: [
|
|
{provide: 'someToken', useFactory: () => new Foo()},
|
|
{provide: 'someToken', useFactory: () => new Foo()},
|
|
{provide: 'someToken', useFactory: () => new Foo()},
|
|
{provide: 'someToken', useFactory: () => new Foo()}
|
|
]
|
|
})
|
|
export class MyModule {}
|
|
`);
|
|
expect(compile()).toEqual(0);
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
expect(mymoduleSource).toContain('ɵ0 = function () { return new Foo(); }');
|
|
expect(mymoduleSource).toContain('ɵ1 = function () { return new Foo(); }');
|
|
expect(mymoduleSource).toContain('ɵ2 = function () { return new Foo(); }');
|
|
expect(mymoduleSource).toContain('ɵ3 = function () { return new Foo(); }');
|
|
expect(mymoduleSource).toContain('export { ɵ0, ɵ1, ɵ2, ɵ3');
|
|
});
|
|
|
|
it('should be able to lower an indirect expression', () => {
|
|
write('mymodule.ts', `
|
|
import {CommonModule} from '@angular/common';
|
|
import {NgModule} from '@angular/core';
|
|
|
|
class Foo {}
|
|
|
|
const factory = () => new Foo();
|
|
|
|
@NgModule({
|
|
imports: [CommonModule],
|
|
providers: [{provide: 'someToken', useFactory: factory}]
|
|
})
|
|
export class MyModule {}
|
|
`);
|
|
expect(compile()).toEqual(0, 'Compile failed');
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
expect(mymoduleSource).toContain('var factory = function () { return new Foo(); }');
|
|
expect(mymoduleSource).toContain('var ɵ0 = factory;');
|
|
expect(mymoduleSource).toContain('export { ɵ0 };');
|
|
});
|
|
|
|
it('should not lower a lambda that is already exported', () => {
|
|
write('mymodule.ts', `
|
|
import {CommonModule} from '@angular/common';
|
|
import {NgModule} from '@angular/core';
|
|
|
|
export class Foo {}
|
|
|
|
export const factory = () => new Foo();
|
|
|
|
@NgModule({
|
|
imports: [CommonModule],
|
|
providers: [{provide: 'someToken', useFactory: factory}]
|
|
})
|
|
export class MyModule {}
|
|
`);
|
|
expect(compile()).toEqual(0);
|
|
|
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
|
expect(mymoduleSource).not.toContain('ɵ0');
|
|
});
|
|
|
|
it('should be able to lower supported expressions', () => {
|
|
writeConfig(`{
|
|
"extends": "./tsconfig-base.json",
|
|
"files": ["module.ts"]
|
|
}`);
|
|
write('module.ts', `
|
|
import {NgModule, InjectionToken} from '@angular/core';
|
|
import {AppComponent} from './app';
|
|
|
|
export interface Info {
|
|
route: string;
|
|
data: string;
|
|
}
|
|
|
|
export const T1 = new InjectionToken<string>('t1');
|
|
export const T2 = new InjectionToken<string>('t2');
|
|
export const T3 = new InjectionToken<number>('t3');
|
|
export const T4 = new InjectionToken<Info[]>('t4');
|
|
|
|
enum SomeEnum {
|
|
OK,
|
|
Cancel
|
|
}
|
|
|
|
function calculateString() {
|
|
return 'someValue';
|
|
}
|
|
|
|
const routeLikeData = [{
|
|
route: '/home',
|
|
data: calculateString()
|
|
}];
|
|
|
|
@NgModule({
|
|
declarations: [AppComponent],
|
|
providers: [
|
|
{ provide: T1, useValue: calculateString() },
|
|
{ provide: T2, useFactory: () => 'someValue' },
|
|
{ provide: T3, useValue: SomeEnum.OK },
|
|
{ provide: T4, useValue: routeLikeData }
|
|
]
|
|
})
|
|
export class MyModule {}
|
|
`);
|
|
write('app.ts', `
|
|
import {Component, Inject} from '@angular/core';
|
|
import * as m from './module';
|
|
|
|
@Component({
|
|
selector: 'my-app',
|
|
template: ''
|
|
})
|
|
export class AppComponent {
|
|
constructor(
|
|
@Inject(m.T1) private t1: string,
|
|
@Inject(m.T2) private t2: string,
|
|
@Inject(m.T3) private t3: number,
|
|
@Inject(m.T4) private t4: m.Info[],
|
|
) {}
|
|
}
|
|
`);
|
|
|
|
expect(main(['-p', basePath], errorSpy)).toBe(0);
|
|
shouldExist('module.js');
|
|
});
|
|
});
|
|
|
|
it('should be able to generate a flat module library', () => {
|
|
writeConfig(`
|
|
{
|
|
"extends": "./tsconfig-base.json",
|
|
"angularCompilerOptions": {
|
|
"flatModuleId": "flat_module",
|
|
"flatModuleOutFile": "index.js",
|
|
"skipTemplateCodegen": true
|
|
},
|
|
"files": ["public-api.ts"]
|
|
}
|
|
`);
|
|
write('public-api.ts', `
|
|
export * from './src/flat.component';
|
|
export * from './src/flat.module';`);
|
|
write('src/flat.component.html', '<div>flat module component</div>');
|
|
write('src/flat.component.ts', `
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'flat-comp',
|
|
templateUrl: 'flat.component.html',
|
|
})
|
|
export class FlatComponent {
|
|
}`);
|
|
write('src/flat.module.ts', `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {FlatComponent} from './flat.component';
|
|
|
|
@NgModule({
|
|
declarations: [
|
|
FlatComponent,
|
|
],
|
|
exports: [
|
|
FlatComponent,
|
|
]
|
|
})
|
|
export class FlatModule {
|
|
}`);
|
|
|
|
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
|
expect(exitCode).toEqual(0);
|
|
shouldExist('index.js');
|
|
shouldExist('index.metadata.json');
|
|
});
|
|
|
|
describe('with tree example', () => {
|
|
beforeEach(() => {
|
|
writeConfig();
|
|
write('index_aot.ts', `
|
|
import {enableProdMode} from '@angular/core';
|
|
import {platformBrowser} from '@angular/platform-browser';
|
|
|
|
import {AppModuleNgFactory} from './tree.ngfactory';
|
|
|
|
enableProdMode();
|
|
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);`);
|
|
write('tree.ts', `
|
|
import {Component, NgModule} from '@angular/core';
|
|
import {CommonModule} from '@angular/common';
|
|
|
|
@Component({
|
|
selector: 'tree',
|
|
inputs: ['data'],
|
|
template:
|
|
\`<span [style.backgroundColor]="bgColor"> {{data.value}} </span><tree *ngIf='data.right != null' [data]='data.right'></tree><tree *ngIf='data.left != null' [data]='data.left'></tree>\`
|
|
})
|
|
export class TreeComponent {
|
|
data: any;
|
|
bgColor = 0;
|
|
}
|
|
|
|
@NgModule({imports: [CommonModule], bootstrap: [TreeComponent], declarations: [TreeComponent]})
|
|
export class AppModule {}
|
|
`);
|
|
});
|
|
|
|
it('should compile without error', () => {
|
|
expect(main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy)).toBe(0);
|
|
});
|
|
});
|
|
|
|
it('should be able to compile multiple libraries with summaries', () => {
|
|
// Note: we need to emit the generated code for the libraries
|
|
// into the node_modules, as that is the only way that we
|
|
// currently support when using summaries.
|
|
// TODO(tbosch): add support for `paths` to our CompilerHost.fileNameToModuleName
|
|
// and then use `paths` here instead of writing to node_modules.
|
|
|
|
// Angular
|
|
write('tsconfig-ng.json', `{
|
|
"extends": "./tsconfig-base.json",
|
|
"angularCompilerOptions": {
|
|
"generateCodeForLibraries": true
|
|
},
|
|
"compilerOptions": {
|
|
"outDir": "."
|
|
},
|
|
"include": ["node_modules/@angular/core/**/*"],
|
|
"exclude": [
|
|
"node_modules/@angular/core/test/**",
|
|
"node_modules/@angular/core/testing/**"
|
|
]
|
|
}`);
|
|
|
|
// Lib 1
|
|
write('lib1/tsconfig-lib1.json', `{
|
|
"extends": "../tsconfig-base.json",
|
|
"angularCompilerOptions": {
|
|
"generateCodeForLibraries": false
|
|
},
|
|
"compilerOptions": {
|
|
"rootDir": ".",
|
|
"outDir": "../node_modules/lib1_built"
|
|
}
|
|
}`);
|
|
write('lib1/module.ts', `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
export function someFactory(): any { return null; }
|
|
|
|
@NgModule({
|
|
providers: [{provide: 'foo', useFactory: someFactory}]
|
|
})
|
|
export class Module {}
|
|
`);
|
|
|
|
// Lib 2
|
|
write('lib2/tsconfig-lib2.json', `{
|
|
"extends": "../tsconfig-base.json",
|
|
"angularCompilerOptions": {
|
|
"generateCodeForLibraries": false
|
|
},
|
|
"compilerOptions": {
|
|
"rootDir": ".",
|
|
"outDir": "../node_modules/lib2_built"
|
|
}
|
|
}`);
|
|
write('lib2/module.ts', `
|
|
export {Module} from 'lib1_built/module';
|
|
`);
|
|
|
|
// Application
|
|
write('app/tsconfig-app.json', `{
|
|
"extends": "../tsconfig-base.json",
|
|
"angularCompilerOptions": {
|
|
"generateCodeForLibraries": false
|
|
},
|
|
"compilerOptions": {
|
|
"rootDir": ".",
|
|
"outDir": "../built/app"
|
|
}
|
|
}`);
|
|
write('app/main.ts', `
|
|
import {NgModule, Inject} from '@angular/core';
|
|
import {Module} from 'lib2_built/module';
|
|
|
|
@NgModule({
|
|
imports: [Module]
|
|
})
|
|
export class AppModule {
|
|
constructor(@Inject('foo') public foo: any) {}
|
|
}
|
|
`);
|
|
|
|
expect(main(['-p', path.join(basePath, 'tsconfig-ng.json')], errorSpy)).toBe(0);
|
|
expect(main(['-p', path.join(basePath, 'lib1', 'tsconfig-lib1.json')], errorSpy)).toBe(0);
|
|
expect(main(['-p', path.join(basePath, 'lib2', 'tsconfig-lib2.json')], errorSpy)).toBe(0);
|
|
expect(main(['-p', path.join(basePath, 'app', 'tsconfig-app.json')], errorSpy)).toBe(0);
|
|
|
|
// library 1
|
|
// make `shouldExist` / `shouldNotExist` relative to `node_modules`
|
|
outDir = path.resolve(basePath, 'node_modules');
|
|
shouldExist('lib1_built/module.js');
|
|
shouldExist('lib1_built/module.ngsummary.json');
|
|
shouldExist('lib1_built/module.ngsummary.js');
|
|
shouldExist('lib1_built/module.ngsummary.d.ts');
|
|
shouldExist('lib1_built/module.ngfactory.js');
|
|
shouldExist('lib1_built/module.ngfactory.d.ts');
|
|
|
|
// library 2
|
|
// make `shouldExist` / `shouldNotExist` relative to `node_modules`
|
|
outDir = path.resolve(basePath, 'node_modules');
|
|
shouldExist('lib2_built/module.js');
|
|
shouldExist('lib2_built/module.ngsummary.json');
|
|
shouldExist('lib2_built/module.ngsummary.js');
|
|
shouldExist('lib2_built/module.ngsummary.d.ts');
|
|
shouldExist('lib2_built/module.ngfactory.js');
|
|
shouldExist('lib2_built/module.ngfactory.d.ts');
|
|
|
|
// app
|
|
// make `shouldExist` / `shouldNotExist` relative to `built`
|
|
outDir = path.resolve(basePath, 'built');
|
|
shouldExist('app/main.js');
|
|
});
|
|
});
|
|
|
|
describe('expression lowering', () => {
|
|
const shouldExist = (fileName: string) => {
|
|
if (!fs.existsSync(path.resolve(basePath, fileName))) {
|
|
throw new Error(`Expected ${fileName} to be emitted (basePath: ${basePath})`);
|
|
}
|
|
};
|
|
|
|
it('should be able to lower supported expressions', () => {
|
|
writeConfig(`{
|
|
"extends": "./tsconfig-base.json",
|
|
"files": ["module.ts"]
|
|
}`);
|
|
write('module.ts', `
|
|
import {NgModule, InjectionToken} from '@angular/core';
|
|
import {AppComponent} from './app';
|
|
|
|
export interface Info {
|
|
route: string;
|
|
data: string;
|
|
}
|
|
|
|
export const T1 = new InjectionToken<string>('t1');
|
|
export const T2 = new InjectionToken<string>('t2');
|
|
export const T3 = new InjectionToken<number>('t3');
|
|
export const T4 = new InjectionToken<Info[]>('t4');
|
|
|
|
enum SomeEnum {
|
|
OK,
|
|
Cancel
|
|
}
|
|
|
|
function calculateString() {
|
|
return 'someValue';
|
|
}
|
|
|
|
const routeLikeData = [{
|
|
route: '/home',
|
|
data: calculateString()
|
|
}];
|
|
|
|
@NgModule({
|
|
declarations: [AppComponent],
|
|
providers: [
|
|
{ provide: T1, useValue: calculateString() },
|
|
{ provide: T2, useFactory: () => 'someValue' },
|
|
{ provide: T3, useValue: SomeEnum.OK },
|
|
{ provide: T4, useValue: routeLikeData }
|
|
]
|
|
})
|
|
export class MyModule {}
|
|
`);
|
|
write('app.ts', `
|
|
import {Component, Inject} from '@angular/core';
|
|
import * as m from './module';
|
|
|
|
@Component({
|
|
selector: 'my-app',
|
|
template: ''
|
|
})
|
|
export class AppComponent {
|
|
constructor(
|
|
@Inject(m.T1) private t1: string,
|
|
@Inject(m.T2) private t2: string,
|
|
@Inject(m.T3) private t3: number,
|
|
@Inject(m.T4) private t4: m.Info[],
|
|
) {}
|
|
}
|
|
`);
|
|
|
|
expect(main(['-p', basePath], s => {})).toBe(0);
|
|
shouldExist('built/module.js');
|
|
});
|
|
});
|
|
|
|
describe('watch mode', () => {
|
|
let timer: (() => void)|undefined = undefined;
|
|
let results: ((message: string) => void)|undefined = undefined;
|
|
let originalTimeout: number;
|
|
|
|
function trigger() {
|
|
const delay = 1000;
|
|
setTimeout(() => {
|
|
const t = timer;
|
|
timer = undefined;
|
|
if (!t) {
|
|
fail('Unexpected state. Timer was not set.');
|
|
} else {
|
|
t();
|
|
}
|
|
}, delay);
|
|
}
|
|
|
|
function whenResults(): Promise<string> {
|
|
return new Promise(resolve => {
|
|
results = message => {
|
|
resolve(message);
|
|
results = undefined;
|
|
};
|
|
});
|
|
}
|
|
|
|
function errorSpy(message: string): void {
|
|
if (results) results(message);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
|
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
|
|
const timerToken = 100;
|
|
spyOn(ts.sys, 'setTimeout').and.callFake((callback: () => void) => {
|
|
timer = callback;
|
|
return timerToken;
|
|
});
|
|
spyOn(ts.sys, 'clearTimeout').and.callFake((token: number) => {
|
|
if (token == timerToken) {
|
|
timer = undefined;
|
|
}
|
|
});
|
|
|
|
write('greet.html', `<p class="greeting"> Hello {{name}}!</p>`);
|
|
write('greet.css', `p.greeting { color: #eee }`);
|
|
write('greet.ts', `
|
|
import {Component, Input} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'greet',
|
|
templateUrl: 'greet.html',
|
|
styleUrls: ['greet.css']
|
|
})
|
|
export class Greet {
|
|
@Input()
|
|
name: string;
|
|
}
|
|
`);
|
|
|
|
write('app.ts', `
|
|
import {Component} from '@angular/core'
|
|
|
|
@Component({
|
|
selector: 'my-app',
|
|
template: \`
|
|
<div>
|
|
<greet [name]='name'></greet>
|
|
</div>
|
|
\`,
|
|
})
|
|
export class App {
|
|
name:string;
|
|
constructor() {
|
|
this.name = \`Angular!\`
|
|
}
|
|
}`);
|
|
|
|
write('module.ts', `
|
|
import {NgModule} from '@angular/core';
|
|
import {Greet} from './greet';
|
|
import {App} from './app';
|
|
|
|
@NgModule({
|
|
declarations: [Greet, App]
|
|
})
|
|
export class MyModule {}
|
|
`);
|
|
});
|
|
|
|
afterEach(() => { jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; });
|
|
|
|
function writeAppConfig(location: string) {
|
|
writeConfig(`{
|
|
"extends": "./tsconfig-base.json",
|
|
"compilerOptions": {
|
|
"outDir": "${location}"
|
|
}
|
|
}`);
|
|
}
|
|
|
|
function expectRecompile(cb: () => void) {
|
|
return (done: DoneFn) => {
|
|
writeAppConfig('dist');
|
|
const config = readCommandLineAndConfiguration(['-p', basePath]);
|
|
const compile = watchMode(config.project, config.options, errorSpy);
|
|
|
|
return new Promise(resolve => {
|
|
compile.ready(() => {
|
|
cb();
|
|
|
|
// Allow the watch callbacks to occur and trigger the timer.
|
|
trigger();
|
|
|
|
// Expect the file to trigger a result.
|
|
whenResults().then(message => {
|
|
expect(message).toMatch(/File change detected/);
|
|
compile.close();
|
|
done();
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
};
|
|
}
|
|
|
|
it('should recompile when config file changes', expectRecompile(() => writeAppConfig('dist2')));
|
|
|
|
it('should recompile when a ts file changes', expectRecompile(() => {
|
|
write('greet.ts', `
|
|
import {Component, Input} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'greet',
|
|
templateUrl: 'greet.html',
|
|
styleUrls: ['greet.css'],
|
|
})
|
|
export class Greet {
|
|
@Input()
|
|
name: string;
|
|
age: number;
|
|
}
|
|
`);
|
|
}));
|
|
|
|
it('should recomiple when the html file changes',
|
|
expectRecompile(() => { write('greet.html', '<p> Hello {{name}} again!</p>'); }));
|
|
|
|
it('should recompile when the css file changes',
|
|
expectRecompile(() => { write('greet.css', `p.greeting { color: blue }`); }));
|
|
});
|
|
});
|