60aeee7abf
Within an @NgModule it's common to include in the imports a call to a ModuleWithProviders function, for example RouterModule.forRoot(). The old ngc compiler was able to handle this pattern because it had global knowledge of metadata of not only the input compilation unit but also all dependencies. The ngtsc compiler for Ivy doesn't have this knowledge, so the pattern of ModuleWithProviders functions is more difficult. ngtsc must be able to determine which module is imported via the function in order to expand the selector scope and properly tree-shake directives and pipes. This commit implements a solution to this problem, by adding a type parameter to ModuleWithProviders through which the actual module type can be passed between compilation units. The provider side isn't a problem because the imports are always copied directly to the ngInjectorDef. PR Close #24862
374 lines
11 KiB
TypeScript
374 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 fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as ts from 'typescript';
|
|
|
|
import {main, readCommandLineAndConfiguration, watchMode} from '../../src/main';
|
|
import {TestSupport, isInBazel, makeTempDir, setup} from '../test_support';
|
|
|
|
function setupFakeCore(support: TestSupport): void {
|
|
const fakeCore = path.join(
|
|
process.env.TEST_SRCDIR, 'angular/packages/compiler-cli/test/ngtsc/fake_core/npm_package');
|
|
|
|
const nodeModulesPath = path.join(support.basePath, 'node_modules');
|
|
const angularCoreDirectory = path.join(nodeModulesPath, '@angular/core');
|
|
|
|
fs.symlinkSync(fakeCore, angularCoreDirectory);
|
|
}
|
|
|
|
function getNgRootDir() {
|
|
const moduleFilename = module.filename.replace(/\\/g, '/');
|
|
const distIndex = moduleFilename.indexOf('/dist/all');
|
|
return moduleFilename.substr(0, distIndex);
|
|
}
|
|
|
|
describe('ngtsc behavioral tests', () => {
|
|
if (!isInBazel()) {
|
|
// These tests should be excluded from the non-Bazel build.
|
|
return;
|
|
}
|
|
|
|
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 getContents(fileName: string): string {
|
|
shouldExist(fileName);
|
|
const modulePath = path.resolve(outDir, fileName);
|
|
return fs.readFileSync(modulePath, 'utf8');
|
|
}
|
|
|
|
function writeConfig(
|
|
tsconfig: string =
|
|
'{"extends": "./tsconfig-base.json", "angularCompilerOptions": {"enableIvy": "ngtsc"}}') {
|
|
write('tsconfig.json', tsconfig);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error);
|
|
const support = setup();
|
|
basePath = support.basePath;
|
|
outDir = path.join(basePath, 'built');
|
|
process.chdir(basePath);
|
|
write = (fileName: string, content: string) => { support.write(fileName, content); };
|
|
|
|
setupFakeCore(support);
|
|
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"]
|
|
},
|
|
"angularCompilerOptions": {
|
|
"enableIvy": "ngtsc"
|
|
}
|
|
}`);
|
|
});
|
|
|
|
it('should compile Injectables without errors', () => {
|
|
writeConfig();
|
|
write('test.ts', `
|
|
import {Injectable} from '@angular/core';
|
|
|
|
@Injectable()
|
|
export class Dep {}
|
|
|
|
@Injectable()
|
|
export class Service {
|
|
constructor(dep: Dep) {}
|
|
}
|
|
`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
expect(exitCode).toBe(0);
|
|
|
|
|
|
const jsContents = getContents('test.js');
|
|
expect(jsContents).toContain('Dep.ngInjectableDef =');
|
|
expect(jsContents).toContain('Service.ngInjectableDef =');
|
|
expect(jsContents).not.toContain('__decorate');
|
|
const dtsContents = getContents('test.d.ts');
|
|
expect(dtsContents).toContain('static ngInjectableDef: i0.InjectableDef<Dep>;');
|
|
expect(dtsContents).toContain('static ngInjectableDef: i0.InjectableDef<Service>;');
|
|
});
|
|
|
|
it('should compile Components without errors', () => {
|
|
writeConfig();
|
|
write('test.ts', `
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: 'this is a test',
|
|
})
|
|
export class TestCmp {}
|
|
`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
expect(exitCode).toBe(0);
|
|
|
|
const jsContents = getContents('test.js');
|
|
expect(jsContents).toContain('TestCmp.ngComponentDef = i0.ɵdefineComponent');
|
|
expect(jsContents).not.toContain('__decorate');
|
|
|
|
const dtsContents = getContents('test.d.ts');
|
|
expect(dtsContents).toContain('static ngComponentDef: i0.ɵComponentDef<TestCmp, \'test-cmp\'>');
|
|
});
|
|
|
|
it('should compile Components without errors', () => {
|
|
writeConfig();
|
|
write('test.ts', `
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
templateUrl: './dir/test.html',
|
|
})
|
|
export class TestCmp {}
|
|
`);
|
|
write('dir/test.html', '<p>Hello World</p>');
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
expect(exitCode).toBe(0);
|
|
|
|
const jsContents = getContents('test.js');
|
|
expect(jsContents).toContain('Hello World');
|
|
});
|
|
|
|
it('should compile NgModules without errors', () => {
|
|
writeConfig();
|
|
write('test.ts', `
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: 'this is a test',
|
|
})
|
|
export class TestCmp {}
|
|
|
|
@NgModule({
|
|
declarations: [TestCmp],
|
|
})
|
|
export class TestModule {}
|
|
`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
expect(exitCode).toBe(0);
|
|
|
|
const jsContents = getContents('test.js');
|
|
expect(jsContents)
|
|
.toContain(
|
|
'i0.ɵdefineNgModule({ type: TestModule, bootstrap: [], ' +
|
|
'declarations: [TestCmp], imports: [], exports: [] })');
|
|
|
|
const dtsContents = getContents('test.d.ts');
|
|
expect(dtsContents).toContain('static ngComponentDef: i0.ɵComponentDef<TestCmp, \'test-cmp\'>');
|
|
expect(dtsContents)
|
|
.toContain('static ngModuleDef: i0.ɵNgModuleDef<TestModule, [TestCmp], [], []>');
|
|
expect(dtsContents).not.toContain('__decorate');
|
|
});
|
|
|
|
it('should compile NgModules with services without errors', () => {
|
|
writeConfig();
|
|
write('test.ts', `
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
export class Token {}
|
|
|
|
@NgModule({})
|
|
export class OtherModule {}
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: 'this is a test',
|
|
})
|
|
export class TestCmp {}
|
|
|
|
@NgModule({
|
|
declarations: [TestCmp],
|
|
providers: [{provide: Token, useValue: 'test'}],
|
|
imports: [OtherModule],
|
|
})
|
|
export class TestModule {}
|
|
`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
expect(exitCode).toBe(0);
|
|
|
|
const jsContents = getContents('test.js');
|
|
expect(jsContents).toContain('i0.ɵdefineNgModule({ type: TestModule,');
|
|
expect(jsContents)
|
|
.toContain(
|
|
`TestModule.ngInjectorDef = i0.defineInjector({ factory: ` +
|
|
`function TestModule_Factory() { return new TestModule(); }, providers: [{ provide: ` +
|
|
`Token, useValue: 'test' }], imports: [OtherModule] });`);
|
|
|
|
const dtsContents = getContents('test.d.ts');
|
|
expect(dtsContents)
|
|
.toContain('static ngModuleDef: i0.ɵNgModuleDef<TestModule, [TestCmp], [OtherModule], []>');
|
|
expect(dtsContents).toContain('static ngInjectorDef: i0.ɵInjectorDef');
|
|
});
|
|
|
|
it('should compile Pipes without errors', () => {
|
|
writeConfig();
|
|
write('test.ts', `
|
|
import {Pipe} from '@angular/core';
|
|
|
|
@Pipe({
|
|
name: 'test-pipe',
|
|
pure: false,
|
|
})
|
|
export class TestPipe {}
|
|
`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
expect(exitCode).toBe(0);
|
|
|
|
const jsContents = getContents('test.js');
|
|
const dtsContents = getContents('test.d.ts');
|
|
|
|
expect(jsContents)
|
|
.toContain(
|
|
'TestPipe.ngPipeDef = i0.ɵdefinePipe({ name: "test-pipe", type: TestPipe, ' +
|
|
'factory: function TestPipe_Factory() { return new TestPipe(); }, pure: false })');
|
|
expect(dtsContents).toContain('static ngPipeDef: i0.ɵPipeDef<TestPipe, \'test-pipe\'>;');
|
|
});
|
|
|
|
it('should compile pure Pipes without errors', () => {
|
|
writeConfig();
|
|
write('test.ts', `
|
|
import {Pipe} from '@angular/core';
|
|
|
|
@Pipe({
|
|
name: 'test-pipe',
|
|
})
|
|
export class TestPipe {}
|
|
`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
expect(exitCode).toBe(0);
|
|
|
|
const jsContents = getContents('test.js');
|
|
const dtsContents = getContents('test.d.ts');
|
|
|
|
expect(jsContents)
|
|
.toContain(
|
|
'TestPipe.ngPipeDef = i0.ɵdefinePipe({ name: "test-pipe", type: TestPipe, ' +
|
|
'factory: function TestPipe_Factory() { return new TestPipe(); }, pure: true })');
|
|
expect(dtsContents).toContain('static ngPipeDef: i0.ɵPipeDef<TestPipe, \'test-pipe\'>;');
|
|
});
|
|
|
|
it('should compile Pipes with dependencies', () => {
|
|
writeConfig();
|
|
write('test.ts', `
|
|
import {Pipe} from '@angular/core';
|
|
|
|
export class Dep {}
|
|
|
|
@Pipe({
|
|
name: 'test-pipe',
|
|
pure: false,
|
|
})
|
|
export class TestPipe {
|
|
constructor(dep: Dep) {}
|
|
}
|
|
`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
expect(exitCode).toBe(0);
|
|
|
|
const jsContents = getContents('test.js');
|
|
expect(jsContents).toContain('return new TestPipe(i0.ɵdirectiveInject(Dep));');
|
|
});
|
|
|
|
it('should include @Pipes in @NgModule scopes', () => {
|
|
writeConfig();
|
|
write('test.ts', `
|
|
import {Component, NgModule, Pipe} from '@angular/core';
|
|
|
|
@Pipe({name: 'test'})
|
|
export class TestPipe {}
|
|
|
|
@Component({selector: 'test-cmp', template: '{{value | test}}'})
|
|
export class TestCmp {}
|
|
|
|
@NgModule({declarations: [TestPipe, TestCmp]})
|
|
export class TestModule {}
|
|
`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
expect(exitCode).toBe(0);
|
|
|
|
const jsContents = getContents('test.js');
|
|
expect(jsContents).toContain('pipes: [TestPipe]');
|
|
|
|
const dtsContents = getContents('test.d.ts');
|
|
expect(dtsContents).toContain('i0.ɵNgModuleDef<TestModule, [TestPipe,TestCmp], [], []>');
|
|
});
|
|
|
|
it('should unwrap a ModuleWithProviders functoin if a generic type is provided for it', () => {
|
|
writeConfig();
|
|
write(`test.ts`, `
|
|
import {NgModule} from '@angular/core';
|
|
import {RouterModule} from 'router';
|
|
|
|
@NgModule({imports: [RouterModule.forRoot()]})
|
|
export class TestModule {}
|
|
`);
|
|
|
|
write('node_modules/router/index.d.ts', `
|
|
import {ModuleWithProviders} from '@angular/core';
|
|
|
|
declare class RouterModule {
|
|
static forRoot(): ModuleWithProviders<RouterModule>;
|
|
}
|
|
`);
|
|
|
|
const exitCode = main(['-p', basePath], errorSpy);
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
expect(exitCode).toBe(0);
|
|
const dtsContents = getContents('test.d.ts');
|
|
expect(dtsContents).toContain(`import * as i1 from 'router';`);
|
|
expect(dtsContents).toContain('i0.ɵNgModuleDef<TestModule, [], [i1.RouterModule], []>');
|
|
});
|
|
});
|