
The error collector changes behavior of the metadata resolver in ways that haven't been fully hardened. This changes limits its use to the lazy route detection and the language service. Issue: #19906 PR Close #19912
834 lines
32 KiB
TypeScript
834 lines
32 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 ng from '@angular/compiler-cli';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as ts from 'typescript';
|
|
|
|
import {CompilerHost, EmitFlags, LazyRoute} from '../../src/transformers/api';
|
|
import {createSrcToOutPathMapper} from '../../src/transformers/program';
|
|
import {GENERATED_FILES, StructureIsReused, tsStructureIsReused} from '../../src/transformers/util';
|
|
import {TestSupport, expectNoDiagnosticsInProgram, setup} from '../test_support';
|
|
|
|
describe('ng program', () => {
|
|
let testSupport: TestSupport;
|
|
let errorSpy: jasmine.Spy&((s: string) => void);
|
|
|
|
beforeEach(() => {
|
|
errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error);
|
|
testSupport = setup();
|
|
});
|
|
|
|
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 {}
|
|
`;
|
|
}
|
|
|
|
function compileLib(libName: string) {
|
|
testSupport.writeFiles({
|
|
[`${libName}_src/index.ts`]: createModuleAndCompSource(libName),
|
|
});
|
|
const options = testSupport.createCompilerOptions();
|
|
const program = ng.createProgram({
|
|
rootNames: [path.resolve(testSupport.basePath, `${libName}_src/index.ts`)],
|
|
options,
|
|
host: ng.createCompilerHost({options}),
|
|
});
|
|
expectNoDiagnosticsInProgram(options, program);
|
|
fs.symlinkSync(
|
|
path.resolve(testSupport.basePath, 'built', `${libName}_src`),
|
|
path.resolve(testSupport.basePath, 'node_modules', libName));
|
|
program.emit({emitFlags: ng.EmitFlags.DTS | ng.EmitFlags.JS | ng.EmitFlags.Metadata});
|
|
}
|
|
|
|
function compile(
|
|
oldProgram?: ng.Program, overrideOptions?: ng.CompilerOptions, rootNames?: string[],
|
|
host?: CompilerHost): {program: ng.Program, emitResult: ts.EmitResult} {
|
|
const options = testSupport.createCompilerOptions(overrideOptions);
|
|
if (!rootNames) {
|
|
rootNames = [path.resolve(testSupport.basePath, 'src/index.ts')];
|
|
}
|
|
if (!host) {
|
|
host = ng.createCompilerHost({options});
|
|
}
|
|
const program = ng.createProgram({
|
|
rootNames: rootNames,
|
|
options,
|
|
host,
|
|
oldProgram,
|
|
});
|
|
expectNoDiagnosticsInProgram(options, program);
|
|
const emitResult = program.emit();
|
|
return {emitResult, program};
|
|
}
|
|
|
|
describe('reuse of old program', () => {
|
|
it('should reuse generated code for libraries from old programs', () => {
|
|
compileLib('lib');
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main'),
|
|
'src/index.ts': `
|
|
export * from './main';
|
|
export * from 'lib/index';
|
|
`
|
|
});
|
|
const p1 = compile().program;
|
|
expect(p1.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib\/.*\.ngfactory\.ts$/.test(sf.fileName)))
|
|
.toBe(true);
|
|
expect(p1.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
const p2 = compile(p1).program;
|
|
expect(p2.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
expect(p2.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
|
|
// import a library for which we didn't generate code before
|
|
compileLib('lib2');
|
|
testSupport.writeFiles({
|
|
'src/index.ts': `
|
|
export * from './main';
|
|
export * from 'lib/index';
|
|
export * from 'lib2/index';
|
|
`,
|
|
});
|
|
const p3 = compile(p2).program;
|
|
expect(p3.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
expect(p3.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib2\/.*\.ngfactory\.ts$/.test(sf.fileName)))
|
|
.toBe(true);
|
|
|
|
const p4 = compile(p3).program;
|
|
expect(p4.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
expect(p4.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
});
|
|
|
|
// Note: this is the case for watch mode with declaration:false
|
|
it('should reuse generated code from libraries from old programs with declaration:false',
|
|
() => {
|
|
compileLib('lib');
|
|
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main'),
|
|
'src/index.ts': `
|
|
export * from './main';
|
|
export * from 'lib/index';
|
|
`
|
|
});
|
|
const p1 = compile(undefined, {declaration: false}).program;
|
|
expect(p1.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib\/.*\.ngfactory\.ts$/.test(sf.fileName)))
|
|
.toBe(true);
|
|
expect(p1.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
const p2 = compile(p1, {declaration: false}).program;
|
|
expect(p2.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
expect(p2.getTsProgram().getSourceFiles().some(
|
|
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
});
|
|
|
|
it('should only emit changed files', () => {
|
|
testSupport.writeFiles({
|
|
'src/index.ts': createModuleAndCompSource('comp', 'index.html'),
|
|
'src/index.html': `Start`
|
|
});
|
|
const options: ng.CompilerOptions = {declaration: false};
|
|
const host = ng.createCompilerHost({options});
|
|
const originalGetSourceFile = host.getSourceFile;
|
|
const fileCache = new Map<string, ts.SourceFile>();
|
|
host.getSourceFile = (fileName: string) => {
|
|
if (fileCache.has(fileName)) {
|
|
return fileCache.get(fileName);
|
|
}
|
|
const sf = originalGetSourceFile.call(host, fileName);
|
|
fileCache.set(fileName, sf);
|
|
return sf;
|
|
};
|
|
|
|
const written = new Map<string, string>();
|
|
host.writeFile = (fileName: string, data: string) => written.set(fileName, data);
|
|
|
|
// compile libraries
|
|
const p1 = compile(undefined, options, undefined, host).program;
|
|
|
|
// compile without libraries
|
|
const p2 = compile(p1, options, undefined, host).program;
|
|
expect(written.has(path.resolve(testSupport.basePath, 'built/src/index.js'))).toBe(true);
|
|
let ngFactoryContent =
|
|
written.get(path.resolve(testSupport.basePath, 'built/src/index.ngfactory.js'));
|
|
expect(ngFactoryContent).toMatch(/Start/);
|
|
|
|
// no change -> no emit
|
|
written.clear();
|
|
const p3 = compile(p2, options, undefined, host).program;
|
|
expect(written.size).toBe(0);
|
|
|
|
// change a user file
|
|
written.clear();
|
|
fileCache.delete(path.resolve(testSupport.basePath, 'src/index.ts'));
|
|
const p4 = compile(p3, options, undefined, host).program;
|
|
expect(written.size).toBe(1);
|
|
expect(written.has(path.resolve(testSupport.basePath, 'built/src/index.js'))).toBe(true);
|
|
|
|
// change a file that is input to generated files
|
|
written.clear();
|
|
testSupport.writeFiles({'src/index.html': 'Hello'});
|
|
const p5 = compile(p4, options, undefined, host).program;
|
|
expect(written.size).toBe(1);
|
|
ngFactoryContent =
|
|
written.get(path.resolve(testSupport.basePath, 'built/src/index.ngfactory.js'));
|
|
expect(ngFactoryContent).toMatch(/Hello/);
|
|
|
|
// change a file and create an intermediate program that is not emitted
|
|
written.clear();
|
|
fileCache.delete(path.resolve(testSupport.basePath, 'src/index.ts'));
|
|
const p6 = ng.createProgram({
|
|
rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')],
|
|
options: testSupport.createCompilerOptions(options), host,
|
|
oldProgram: p5
|
|
});
|
|
const p7 = compile(p6, options, undefined, host).program;
|
|
expect(written.size).toBe(1);
|
|
});
|
|
|
|
it('should set emitSkipped to false for full and incremental emit', () => {
|
|
testSupport.writeFiles({
|
|
'src/index.ts': createModuleAndCompSource('main'),
|
|
});
|
|
const {emitResult: emitResult1, program: p1} = compile();
|
|
expect(emitResult1.emitSkipped).toBe(false);
|
|
const {emitResult: emitResult2, program: p2} = compile(p1);
|
|
expect(emitResult2.emitSkipped).toBe(false);
|
|
const {emitResult: emitResult3, program: p3} = compile(p2);
|
|
expect(emitResult3.emitSkipped).toBe(false);
|
|
});
|
|
|
|
it('should store library summaries on emit', () => {
|
|
compileLib('lib');
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main'),
|
|
'src/index.ts': `
|
|
export * from './main';
|
|
export * from 'lib/index';
|
|
`
|
|
});
|
|
const p1 = compile().program;
|
|
expect(Array.from(p1.getLibrarySummaries().values())
|
|
.some(sf => /node_modules\/lib\/index\.ngfactory\.d\.ts$/.test(sf.fileName)))
|
|
.toBe(true);
|
|
expect(Array.from(p1.getLibrarySummaries().values())
|
|
.some(sf => /node_modules\/lib\/index\.ngsummary\.json$/.test(sf.fileName)))
|
|
.toBe(true);
|
|
expect(Array.from(p1.getLibrarySummaries().values())
|
|
.some(sf => /node_modules\/lib\/index\.d\.ts$/.test(sf.fileName)))
|
|
.toBe(true);
|
|
|
|
expect(Array.from(p1.getLibrarySummaries().values())
|
|
.some(sf => /src\/main.*$/.test(sf.fileName)))
|
|
.toBe(false);
|
|
});
|
|
|
|
it('should reuse the old ts program completely if nothing changed', () => {
|
|
testSupport.writeFiles({'src/index.ts': createModuleAndCompSource('main')});
|
|
// Note: the second compile drops factories for library files,
|
|
// and therefore changes the structure again
|
|
const p1 = compile().program;
|
|
const p2 = compile(p1).program;
|
|
compile(p2);
|
|
expect(tsStructureIsReused(p2.getTsProgram())).toBe(StructureIsReused.Completely);
|
|
});
|
|
|
|
it('should reuse the old ts program completely if a template or a ts file changed', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main', 'main.html'),
|
|
'src/main.html': `Some template`,
|
|
'src/util.ts': `export const x = 1`,
|
|
'src/index.ts': `
|
|
export * from './main';
|
|
export * from './util';
|
|
`
|
|
});
|
|
// Note: the second compile drops factories for library files,
|
|
// and therefore changes the structure again
|
|
const p1 = compile().program;
|
|
const p2 = compile(p1).program;
|
|
testSupport.writeFiles({
|
|
'src/main.html': `Another template`,
|
|
'src/util.ts': `export const x = 2`,
|
|
});
|
|
compile(p2);
|
|
expect(tsStructureIsReused(p2.getTsProgram())).toBe(StructureIsReused.Completely);
|
|
});
|
|
|
|
it('should not reuse the old ts program if an import changed', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main'),
|
|
'src/util.ts': `export const x = 1`,
|
|
'src/index.ts': `
|
|
export * from './main';
|
|
export * from './util';
|
|
`
|
|
});
|
|
// Note: the second compile drops factories for library files,
|
|
// and therefore changes the structure again
|
|
const p1 = compile().program;
|
|
const p2 = compile(p1).program;
|
|
testSupport.writeFiles(
|
|
{'src/util.ts': `import {Injectable} from '@angular/core'; export const x = 1;`});
|
|
compile(p2);
|
|
expect(tsStructureIsReused(p2.getTsProgram())).toBe(StructureIsReused.SafeModules);
|
|
});
|
|
});
|
|
|
|
it('should not typecheck templates if skipTemplateCodegen is set but fullTemplateTypeCheck is not',
|
|
() => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
@NgModule(() => {if (1==1) return null as any;})
|
|
export class SomeClassWithInvalidMetadata {}
|
|
`,
|
|
});
|
|
const options = testSupport.createCompilerOptions({skipTemplateCodegen: true});
|
|
const host = ng.createCompilerHost({options});
|
|
const program = ng.createProgram(
|
|
{rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host});
|
|
expectNoDiagnosticsInProgram(options, program);
|
|
const emitResult = program.emit({emitFlags: EmitFlags.All});
|
|
expect(emitResult.diagnostics.length).toBe(0);
|
|
|
|
testSupport.shouldExist('built/src/main.metadata.json');
|
|
});
|
|
|
|
it('should typecheck templates if skipTemplateCodegen and fullTemplateTypeCheck is set', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main', `{{nonExistent}}`),
|
|
});
|
|
const options = testSupport.createCompilerOptions({
|
|
skipTemplateCodegen: true,
|
|
fullTemplateTypeCheck: true,
|
|
});
|
|
const host = ng.createCompilerHost({options});
|
|
const program = ng.createProgram(
|
|
{rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host});
|
|
const diags = program.getNgSemanticDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].messageText).toBe(`Property 'nonExistent' does not exist on type 'mainComp'.`);
|
|
});
|
|
|
|
it('should be able to use asynchronously loaded resources', (done) => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main', 'main.html'),
|
|
// Note: we need to be able to resolve the template synchronously,
|
|
// only the content is delivered asynchronously.
|
|
'src/main.html': '',
|
|
});
|
|
const options = testSupport.createCompilerOptions();
|
|
const host = ng.createCompilerHost({options});
|
|
host.readResource = () => Promise.resolve('Hello world!');
|
|
const program = ng.createProgram(
|
|
{rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host});
|
|
program.loadNgStructureAsync().then(() => {
|
|
program.emit();
|
|
const factory =
|
|
fs.readFileSync(path.resolve(testSupport.basePath, 'built/src/main.ngfactory.js'));
|
|
expect(factory).toContain('Hello world!');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should work with noResolve', () => {
|
|
// create a temporary ts program to get the list of all files from angular...
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main'),
|
|
});
|
|
const preOptions = testSupport.createCompilerOptions();
|
|
const preHost = ts.createCompilerHost(preOptions);
|
|
// don't resolve symlinks
|
|
preHost.realpath = (f) => f;
|
|
const preProgram =
|
|
ts.createProgram([path.resolve(testSupport.basePath, 'src/main.ts')], preOptions, preHost);
|
|
const allRootNames = preProgram.getSourceFiles().map(sf => sf.fileName);
|
|
|
|
// now do the actual test with noResolve
|
|
const program = compile(undefined, {noResolve: true}, allRootNames);
|
|
|
|
testSupport.shouldExist('built/src/main.ngfactory.js');
|
|
testSupport.shouldExist('built/src/main.ngfactory.d.ts');
|
|
});
|
|
|
|
it('should emit also empty generated files depending on the options', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': `
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
@Component({selector: 'main', template: '', styleUrls: ['main.css']})
|
|
export class MainComp {}
|
|
|
|
@NgModule({declarations: [MainComp]})
|
|
export class MainModule {}
|
|
`,
|
|
'src/main.css': ``,
|
|
'src/util.ts': 'export const x = 1;',
|
|
'src/index.ts': `
|
|
export * from './util';
|
|
export * from './main';
|
|
`,
|
|
});
|
|
const options = testSupport.createCompilerOptions({
|
|
allowEmptyCodegenFiles: true,
|
|
enableSummariesForJit: true,
|
|
});
|
|
const host = ng.createCompilerHost({options});
|
|
const written = new Map < string, {
|
|
original: ts.SourceFile[]|undefined;
|
|
data: string;
|
|
}
|
|
> ();
|
|
|
|
host.writeFile =
|
|
(fileName: string, data: string, writeByteOrderMark: boolean,
|
|
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => {
|
|
written.set(fileName, {original: sourceFiles, data});
|
|
};
|
|
const program = ng.createProgram(
|
|
{rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')], options, host});
|
|
program.emit();
|
|
|
|
function assertGenFile(
|
|
fileName: string, checks: {originalFileName: string, shouldBeEmpty: boolean}) {
|
|
const writeData = written.get(path.join(testSupport.basePath, fileName));
|
|
expect(writeData).toBeTruthy();
|
|
expect(writeData !.original !.some(
|
|
sf => sf.fileName === path.join(testSupport.basePath, checks.originalFileName)))
|
|
.toBe(true);
|
|
if (checks.shouldBeEmpty) {
|
|
expect(writeData !.data).toBe('');
|
|
} else {
|
|
expect(writeData !.data).not.toBe('');
|
|
}
|
|
}
|
|
|
|
assertGenFile(
|
|
'built/src/util.ngfactory.js', {originalFileName: 'src/util.ts', shouldBeEmpty: true});
|
|
assertGenFile(
|
|
'built/src/util.ngfactory.d.ts', {originalFileName: 'src/util.ts', shouldBeEmpty: true});
|
|
assertGenFile(
|
|
'built/src/util.ngsummary.js', {originalFileName: 'src/util.ts', shouldBeEmpty: true});
|
|
assertGenFile(
|
|
'built/src/util.ngsummary.d.ts', {originalFileName: 'src/util.ts', shouldBeEmpty: true});
|
|
assertGenFile(
|
|
'built/src/util.ngsummary.json', {originalFileName: 'src/util.ts', shouldBeEmpty: false});
|
|
|
|
// Note: we always fill non shim and shim style files as they might
|
|
// be shared by component with and without ViewEncapsulation.
|
|
assertGenFile(
|
|
'built/src/main.css.ngstyle.js', {originalFileName: 'src/main.ts', shouldBeEmpty: false});
|
|
assertGenFile(
|
|
'built/src/main.css.ngstyle.d.ts', {originalFileName: 'src/main.ts', shouldBeEmpty: true});
|
|
// Note: this file is not empty as we actually generated code for it
|
|
assertGenFile(
|
|
'built/src/main.css.shim.ngstyle.js',
|
|
{originalFileName: 'src/main.ts', shouldBeEmpty: false});
|
|
assertGenFile(
|
|
'built/src/main.css.shim.ngstyle.d.ts',
|
|
{originalFileName: 'src/main.ts', shouldBeEmpty: true});
|
|
});
|
|
|
|
it('should not emit /// references in .d.ts files', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main'),
|
|
});
|
|
compile(undefined, {declaration: true}, [path.resolve(testSupport.basePath, 'src/main.ts')]);
|
|
|
|
const dts =
|
|
fs.readFileSync(path.resolve(testSupport.basePath, 'built', 'src', 'main.d.ts')).toString();
|
|
expect(dts).toMatch('export declare class');
|
|
expect(dts).not.toMatch('///');
|
|
});
|
|
|
|
it('should not emit generated files whose sources are outside of the rootDir', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': createModuleAndCompSource('main'),
|
|
'src/index.ts': `
|
|
export * from './main';
|
|
`
|
|
});
|
|
const options =
|
|
testSupport.createCompilerOptions({rootDir: path.resolve(testSupport.basePath, 'src')});
|
|
const host = ng.createCompilerHost({options});
|
|
const writtenFileNames: string[] = [];
|
|
const oldWriteFile = host.writeFile;
|
|
host.writeFile = (fileName, data, writeByteOrderMark) => {
|
|
writtenFileNames.push(fileName);
|
|
oldWriteFile(fileName, data, writeByteOrderMark);
|
|
};
|
|
|
|
compile(/*oldProgram*/ undefined, options, /*rootNames*/ undefined, host);
|
|
|
|
// no emit for files from node_modules as they are outside of rootDir
|
|
expect(writtenFileNames.some(f => /node_modules/.test(f))).toBe(false);
|
|
|
|
// emit all gen files for files under src/
|
|
testSupport.shouldExist('built/main.js');
|
|
testSupport.shouldExist('built/main.d.ts');
|
|
testSupport.shouldExist('built/main.ngfactory.js');
|
|
testSupport.shouldExist('built/main.ngfactory.d.ts');
|
|
testSupport.shouldExist('built/main.ngsummary.json');
|
|
});
|
|
|
|
describe('createSrcToOutPathMapper', () => {
|
|
it('should return identity mapping if no outDir is present', () => {
|
|
const mapper = createSrcToOutPathMapper(undefined, undefined, undefined);
|
|
expect(mapper('/tmp/b/y.js')).toBe('/tmp/b/y.js');
|
|
});
|
|
|
|
it('should return identity mapping if first src and out fileName have same dir', () => {
|
|
const mapper = createSrcToOutPathMapper('/tmp', '/tmp/a/x.ts', '/tmp/a/x.js');
|
|
expect(mapper('/tmp/b/y.js')).toBe('/tmp/b/y.js');
|
|
});
|
|
|
|
it('should adjust the filename if the outDir is inside of the rootDir', () => {
|
|
const mapper = createSrcToOutPathMapper('/tmp/out', '/tmp/a/x.ts', '/tmp/out/a/x.js');
|
|
expect(mapper('/tmp/b/y.js')).toBe('/tmp/out/b/y.js');
|
|
});
|
|
|
|
it('should adjust the filename if the outDir is outside of the rootDir', () => {
|
|
const mapper = createSrcToOutPathMapper('/out', '/tmp/a/x.ts', '/out/a/x.js');
|
|
expect(mapper('/tmp/b/y.js')).toBe('/out/b/y.js');
|
|
});
|
|
|
|
it('should adjust the filename if the common prefix of sampleSrc and sampleOut is outside of outDir',
|
|
() => {
|
|
const mapper =
|
|
createSrcToOutPathMapper('/dist/common', '/src/common/x.ts', '/dist/common/x.js');
|
|
expect(mapper('/src/common/y.js')).toBe('/dist/common/y.js');
|
|
});
|
|
|
|
it('should work on windows with normalized paths', () => {
|
|
const mapper =
|
|
createSrcToOutPathMapper('c:/tmp/out', 'c:/tmp/a/x.ts', 'c:/tmp/out/a/x.js', path.win32);
|
|
expect(mapper('c:/tmp/b/y.js')).toBe('c:\\tmp\\out\\b\\y.js');
|
|
});
|
|
|
|
it('should work on windows with non-normalized paths', () => {
|
|
const mapper = createSrcToOutPathMapper(
|
|
'c:\\tmp\\out', 'c:\\tmp\\a\\x.ts', 'c:\\tmp\\out\\a\\x.js', path.win32);
|
|
expect(mapper('c:\\tmp\\b\\y.js')).toBe('c:\\tmp\\out\\b\\y.js');
|
|
});
|
|
});
|
|
|
|
describe('listLazyRoutes', () => {
|
|
function writeSomeRoutes() {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
import {RouterModule} from '@angular/router';
|
|
|
|
@NgModule({
|
|
imports: [RouterModule.forRoot([{loadChildren: './child#ChildModule'}])]
|
|
})
|
|
export class MainModule {}
|
|
`,
|
|
'src/child.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
import {RouterModule} from '@angular/router';
|
|
|
|
@NgModule({
|
|
imports: [RouterModule.forChild([{loadChildren: './child2#ChildModule2'}])]
|
|
})
|
|
export class ChildModule {}
|
|
`,
|
|
'src/child2.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
@NgModule()
|
|
export class ChildModule2 {}
|
|
`,
|
|
});
|
|
}
|
|
|
|
function createProgram(rootNames: string[], overrideOptions: ng.CompilerOptions = {}) {
|
|
const options = testSupport.createCompilerOptions(overrideOptions);
|
|
const host = ng.createCompilerHost({options});
|
|
const program = ng.createProgram(
|
|
{rootNames: rootNames.map(p => path.resolve(testSupport.basePath, p)), options, host});
|
|
return {program, options};
|
|
}
|
|
|
|
function normalizeRoutes(lazyRoutes: LazyRoute[]) {
|
|
return lazyRoutes.map(
|
|
r => ({
|
|
route: r.route,
|
|
module: {name: r.module.name, filePath: r.module.filePath},
|
|
referencedModule:
|
|
{name: r.referencedModule.name, filePath: r.referencedModule.filePath},
|
|
}));
|
|
}
|
|
|
|
it('should list all lazyRoutes', () => {
|
|
writeSomeRoutes();
|
|
const {program, options} = createProgram(['src/main.ts', 'src/child.ts', 'src/child2.ts']);
|
|
expectNoDiagnosticsInProgram(options, program);
|
|
expect(normalizeRoutes(program.listLazyRoutes())).toEqual([
|
|
{
|
|
module: {name: 'MainModule', filePath: path.resolve(testSupport.basePath, 'src/main.ts')},
|
|
referencedModule:
|
|
{name: 'ChildModule', filePath: path.resolve(testSupport.basePath, 'src/child.ts')},
|
|
route: './child#ChildModule'
|
|
},
|
|
{
|
|
module:
|
|
{name: 'ChildModule', filePath: path.resolve(testSupport.basePath, 'src/child.ts')},
|
|
referencedModule:
|
|
{name: 'ChildModule2', filePath: path.resolve(testSupport.basePath, 'src/child2.ts')},
|
|
route: './child2#ChildModule2'
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should list lazyRoutes given an entryRoute recursively', () => {
|
|
writeSomeRoutes();
|
|
const {program, options} = createProgram(['src/main.ts']);
|
|
expectNoDiagnosticsInProgram(options, program);
|
|
expect(normalizeRoutes(program.listLazyRoutes('src/main#MainModule'))).toEqual([
|
|
{
|
|
module: {name: 'MainModule', filePath: path.resolve(testSupport.basePath, 'src/main.ts')},
|
|
referencedModule:
|
|
{name: 'ChildModule', filePath: path.resolve(testSupport.basePath, 'src/child.ts')},
|
|
route: './child#ChildModule'
|
|
},
|
|
{
|
|
module:
|
|
{name: 'ChildModule', filePath: path.resolve(testSupport.basePath, 'src/child.ts')},
|
|
referencedModule:
|
|
{name: 'ChildModule2', filePath: path.resolve(testSupport.basePath, 'src/child2.ts')},
|
|
route: './child2#ChildModule2'
|
|
},
|
|
]);
|
|
|
|
expect(normalizeRoutes(program.listLazyRoutes('src/child#ChildModule'))).toEqual([
|
|
{
|
|
module:
|
|
{name: 'ChildModule', filePath: path.resolve(testSupport.basePath, 'src/child.ts')},
|
|
referencedModule:
|
|
{name: 'ChildModule2', filePath: path.resolve(testSupport.basePath, 'src/child2.ts')},
|
|
route: './child2#ChildModule2'
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should list lazyRoutes pointing to a default export', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
import {RouterModule} from '@angular/router';
|
|
|
|
@NgModule({
|
|
imports: [RouterModule.forRoot([{loadChildren: './child'}])]
|
|
})
|
|
export class MainModule {}
|
|
`,
|
|
'src/child.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
@NgModule()
|
|
export default class ChildModule {}
|
|
`,
|
|
});
|
|
const {program, options} = createProgram(['src/main.ts']);
|
|
expect(normalizeRoutes(program.listLazyRoutes('src/main#MainModule'))).toEqual([
|
|
{
|
|
module: {name: 'MainModule', filePath: path.resolve(testSupport.basePath, 'src/main.ts')},
|
|
referencedModule:
|
|
{name: undefined, filePath: path.resolve(testSupport.basePath, 'src/child.ts')},
|
|
route: './child'
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should list lazyRoutes from imported modules', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
import {RouterModule} from '@angular/router';
|
|
import {NestedMainModule} from './nested/main';
|
|
|
|
@NgModule({
|
|
imports: [
|
|
RouterModule.forRoot([{loadChildren: './child#ChildModule'}]),
|
|
NestedMainModule,
|
|
]
|
|
})
|
|
export class MainModule {}
|
|
`,
|
|
'src/child.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
@NgModule()
|
|
export class ChildModule {}
|
|
`,
|
|
'src/nested/main.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
import {RouterModule} from '@angular/router';
|
|
|
|
@NgModule({
|
|
imports: [RouterModule.forChild([{loadChildren: './child#NestedChildModule'}])]
|
|
})
|
|
export class NestedMainModule {}
|
|
`,
|
|
'src/nested/child.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
@NgModule()
|
|
export class NestedChildModule {}
|
|
`,
|
|
});
|
|
const {program, options} = createProgram(['src/main.ts']);
|
|
expect(normalizeRoutes(program.listLazyRoutes('src/main#MainModule'))).toEqual([
|
|
{
|
|
module: {
|
|
name: 'NestedMainModule',
|
|
filePath: path.resolve(testSupport.basePath, 'src/nested/main.ts')
|
|
},
|
|
referencedModule: {
|
|
name: 'NestedChildModule',
|
|
filePath: path.resolve(testSupport.basePath, 'src/nested/child.ts')
|
|
},
|
|
route: './child#NestedChildModule'
|
|
},
|
|
{
|
|
module: {name: 'MainModule', filePath: path.resolve(testSupport.basePath, 'src/main.ts')},
|
|
referencedModule:
|
|
{name: 'ChildModule', filePath: path.resolve(testSupport.basePath, 'src/child.ts')},
|
|
route: './child#ChildModule'
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should dedupe lazyRoutes given an entryRoute', () => {
|
|
writeSomeRoutes();
|
|
testSupport.writeFiles({
|
|
'src/index.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
import {RouterModule} from '@angular/router';
|
|
|
|
@NgModule({
|
|
imports: [
|
|
RouterModule.forRoot([{loadChildren: './main#MainModule'}]),
|
|
RouterModule.forRoot([{loadChildren: './child#ChildModule'}]),
|
|
]
|
|
})
|
|
export class MainModule {}
|
|
`,
|
|
});
|
|
const {program, options} = createProgram(['src/index.ts']);
|
|
expectNoDiagnosticsInProgram(options, program);
|
|
expect(normalizeRoutes(program.listLazyRoutes('src/main#MainModule'))).toEqual([
|
|
{
|
|
module: {name: 'MainModule', filePath: path.resolve(testSupport.basePath, 'src/main.ts')},
|
|
referencedModule:
|
|
{name: 'ChildModule', filePath: path.resolve(testSupport.basePath, 'src/child.ts')},
|
|
route: './child#ChildModule'
|
|
},
|
|
{
|
|
module:
|
|
{name: 'ChildModule', filePath: path.resolve(testSupport.basePath, 'src/child.ts')},
|
|
referencedModule:
|
|
{name: 'ChildModule2', filePath: path.resolve(testSupport.basePath, 'src/child2.ts')},
|
|
route: './child2#ChildModule2'
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should list lazyRoutes given an entryRoute even with static errors', () => {
|
|
testSupport.writeFiles({
|
|
'src/main.ts': `
|
|
import {NgModule, Component} from '@angular/core';
|
|
import {RouterModule} from '@angular/router';
|
|
|
|
@Component({
|
|
selector: 'url-comp',
|
|
// Non existent external template
|
|
templateUrl: 'non-existent.html',
|
|
})
|
|
export class ErrorComp {}
|
|
|
|
@Component({
|
|
selector: 'err-comp',
|
|
// Error in template
|
|
template: '<input/>{{',
|
|
})
|
|
export class ErrorComp2 {}
|
|
|
|
// Component with metadata errors.
|
|
@Component(() => {if (1==1) return null as any;})
|
|
export class ErrorComp3 {}
|
|
|
|
// Unused component
|
|
@Component({
|
|
selector: 'unused-comp',
|
|
template: ''
|
|
})
|
|
export class UnusedComp {}
|
|
|
|
@NgModule({
|
|
declarations: [ErrorComp, ErrorComp2, ErrorComp3, NonExistentComp],
|
|
imports: [RouterModule.forRoot([{loadChildren: './child#ChildModule'}])]
|
|
})
|
|
export class MainModule {}
|
|
|
|
@NgModule({
|
|
// Component used in 2 NgModules
|
|
declarations: [ErrorComp],
|
|
})
|
|
export class Mod2 {}
|
|
`,
|
|
'src/child.ts': `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
@NgModule()
|
|
export class ChildModule {}
|
|
`,
|
|
});
|
|
const program = createProgram(['src/main.ts'], {collectAllErrors: true}).program;
|
|
expect(normalizeRoutes(program.listLazyRoutes('src/main#MainModule'))).toEqual([{
|
|
module: {name: 'MainModule', filePath: path.resolve(testSupport.basePath, 'src/main.ts')},
|
|
referencedModule:
|
|
{name: 'ChildModule', filePath: path.resolve(testSupport.basePath, 'src/child.ts')},
|
|
route: './child#ChildModule'
|
|
}]);
|
|
});
|
|
});
|
|
});
|