/** * @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 {formatDiagnostics} from '../../src/perform_compile'; 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}; } function resolveFiles(rootNames: string[]) { const preOptions = testSupport.createCompilerOptions(); const preHost = ts.createCompilerHost(preOptions); // don't resolve symlinks preHost.realpath = (f) => f; const preProgram = ts.createProgram(rootNames, preOptions, preHost); return preProgram.getSourceFiles().map(sf => sf.fileName); } 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(); 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(); 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;}) 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 allRootNames = resolveFiles([path.resolve(testSupport.basePath, 'src/main.ts')]); // 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 work with tsx files', () => { // create a temporary ts program to get the list of all files from angular... testSupport.writeFiles({ 'src/main.tsx': createModuleAndCompSource('main'), }); const allRootNames = resolveFiles([path.resolve(testSupport.basePath, 'src/main.tsx')]); const program = compile(undefined, {jsx: ts.JsxEmit.React}, allRootNames); testSupport.shouldExist('built/src/main.js'); testSupport.shouldExist('built/src/main.d.ts'); testSupport.shouldExist('built/src/main.ngfactory.js'); testSupport.shouldExist('built/src/main.ngfactory.d.ts'); testSupport.shouldExist('built/src/main.ngsummary.json'); }); 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) { // The file should only contain comments (the preamble comment added by ngc). expect(writeData !.data).toMatch(/^(\s*\/\*([^*]|\*[^/])*\*\/\s*)?$/); } 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 emit correctly after listing lazyRoutes', () => { testSupport.writeFiles({ 'src/main.ts': ` import {NgModule} from '@angular/core'; import {RouterModule} from '@angular/router'; @NgModule({ imports: [RouterModule.forRoot([{loadChildren: './lazy/lazy#LazyModule'}])] }) export class MainModule {} `, 'src/lazy/lazy.ts': ` import {NgModule} from '@angular/core'; @NgModule() export class ChildModule {} `, }); const {program, options} = createProgram(['src/main.ts', 'src/lazy/lazy.ts']); expectNoDiagnosticsInProgram(options, program); program.listLazyRoutes(); program.emit(); const lazyNgFactory = fs.readFileSync(path.resolve(testSupport.basePath, 'built/src/lazy/lazy.ngfactory.js')); expect(lazyNgFactory).toContain('import * as i1 from "./lazy";'); }); 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: '{{', }) 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' }]); }); }); it('should report errors for ts and ng errors on emit with noEmitOnError=true', () => { testSupport.writeFiles({ 'src/main.ts': ` import {Component, NgModule} from '@angular/core'; // Ts error let x: string = 1; // Ng error @Component({selector: 'comp', templateUrl: './main.html'}) export class MyComp {} @NgModule({declarations: [MyComp]}) export class MyModule {} `, 'src/main.html': '{{nonExistent}}' }); const options = testSupport.createCompilerOptions({noEmitOnError: true}); const host = ng.createCompilerHost({options}); const program1 = ng.createProgram( {rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host}); const errorDiags = program1.emit().diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error); expect(formatDiagnostics(errorDiags)) .toContain(`src/main.ts(5,13): error TS2322: Type '1' is not assignable to type 'string'.`); expect(formatDiagnostics(errorDiags)) .toContain( `src/main.html(1,1): error TS100: Property 'nonExistent' does not exist on type 'MyComp'.`); }); it('should not report emit errors with noEmitOnError=false', () => { testSupport.writeFiles({ 'src/main.ts': ` @NgModule() ` }); const options = testSupport.createCompilerOptions({noEmitOnError: false}); const host = ng.createCompilerHost({options}); const program1 = ng.createProgram( {rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host}); expect(program1.emit().diagnostics.length).toBe(0); }); describe('errors', () => { const fileWithStructuralError = ` import {NgModule} from '@angular/core'; @NgModule(() => (1===1 ? null as any : null as any)) export class MyModule {} `; const fileWithGoodContent = ` import {NgModule} from '@angular/core'; @NgModule() export class MyModule {} `; it('should not throw on structural errors but collect them', () => { testSupport.write('src/index.ts', fileWithStructuralError); const options = testSupport.createCompilerOptions(); const host = ng.createCompilerHost({options}); const program = ng.createProgram( {rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')], options, host}); const structuralErrors = program.getNgStructuralDiagnostics(); expect(structuralErrors.length).toBe(1); expect(structuralErrors[0].messageText).toContain('Function expressions are not supported'); }); it('should not throw on structural errors but collect them (loadNgStructureAsync)', (done) => { testSupport.write('src/index.ts', fileWithStructuralError); const options = testSupport.createCompilerOptions(); const host = ng.createCompilerHost({options}); const program = ng.createProgram( {rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')], options, host}); program.loadNgStructureAsync().then(() => { const structuralErrors = program.getNgStructuralDiagnostics(); expect(structuralErrors.length).toBe(1); expect(structuralErrors[0].messageText).toContain('Function expressions are not supported'); done(); }); }); it('should include non-formatted errors (e.g. invalid templateUrl)', () => { testSupport.write('src/index.ts', ` import {Component, NgModule} from '@angular/core'; @Component({ selector: 'my-component', templateUrl: 'template.html', // invalid template url }) export class MyComponent {} @NgModule({ declarations: [MyComponent] }) export class MyModule {} `); const options = testSupport.createCompilerOptions(); const host = ng.createCompilerHost({options}); const program = ng.createProgram({ rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')], options, host, }); const structuralErrors = program.getNgStructuralDiagnostics(); expect(structuralErrors.length).toBe(1); expect(structuralErrors[0].messageText).toContain('Couldn\'t resolve resource template.html'); }); it('should be able report structural errors with noResolve:true and generateCodeForLibraries:false ' + 'even if getSourceFile throws for non existent files', () => { testSupport.write('src/index.ts', fileWithGoodContent); // compile angular and produce .ngsummary.json / ngfactory.d.ts files compile(); testSupport.write('src/ok.ts', fileWithGoodContent); testSupport.write('src/error.ts', fileWithStructuralError); // Make sure the ok.ts file is before the error.ts file, // so we added a .ngfactory.ts file for it. const allRootNames = resolveFiles( ['src/ok.ts', 'src/error.ts'].map(fn => path.resolve(testSupport.basePath, fn))); const options = testSupport.createCompilerOptions({ noResolve: true, generateCodeForLibraries: false, }); const host = ng.createCompilerHost({options}); const originalGetSourceFile = host.getSourceFile; host.getSourceFile = (fileName: string, languageVersion: ts.ScriptTarget, onError?: ((message: string) => void) | undefined): ts.SourceFile => { // We should never try to load .ngfactory.ts files if (fileName.match(/\.ngfactory\.ts$/)) { throw new Error(`Non existent ngfactory file: ` + fileName); } return originalGetSourceFile.call(host, fileName, languageVersion, onError); }; const program = ng.createProgram({rootNames: allRootNames, options, host}); const structuralErrors = program.getNgStructuralDiagnostics(); expect(structuralErrors.length).toBe(1); expect(structuralErrors[0].messageText) .toContain('Function expressions are not supported'); }); }); });