/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {AotSummaryResolver, GeneratedFile, StaticSymbolCache, StaticSymbolResolver, toTypeScript} from '@angular/compiler'; import {MetadataBundler} from '@angular/compiler-cli/src/metadata/bundler'; import {privateEntriesToIndex} from '@angular/compiler-cli/src/metadata/index_writer'; import {extractSourceMap, originalPositionFor} from '@angular/compiler/testing/src/output/source_map_util'; import {NodeFlags} from '@angular/core/src/view/index'; import * as ts from 'typescript'; import {arrayToMockDir, compile, EmittingCompilerHost, expectNoDiagnostics, isInBazel, MockAotCompilerHost, MockCompilerHost, MockDirectory, MockMetadataBundlerHost, settings, setup, toMockFileArray} from './test_util'; describe('compiler (unbundled Angular)', () => { let angularFiles = setup(); describe('Quickstart', () => { it('should compile', () => { const {genFiles} = compile([QUICKSTART, angularFiles]); expect(genFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl))).toBeDefined(); expect(genFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl))).toBeDefined(); }); }); describe('aot source mapping', () => { const componentPath = '/app/app.component.ts'; const ngFactoryPath = '/app/app.component.ngfactory.ts'; let rootDir: MockDirectory; let appDir: MockDirectory; beforeEach(() => { appDir = { 'app.module.ts': ` import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], bootstrap: [ AppComponent ] }) export class AppModule { } ` }; rootDir = {'app': appDir}; }); function compileApp(): GeneratedFile { const {genFiles} = compile([rootDir, angularFiles]); return genFiles.find( genFile => genFile.srcFileUrl === componentPath && genFile.genFileUrl.endsWith('.ts'))!; } function findLineAndColumn( file: string, token: string): {line: number|null, column: number|null} { const index = file.indexOf(token); if (index === -1) { return {line: null, column: null}; } const linesUntilToken = file.slice(0, index).split('\n'); const line = linesUntilToken.length; const column = linesUntilToken[linesUntilToken.length - 1].length; return {line, column}; } function createComponentSource(componentDecorator: string) { return ` import { NgModule, Component } from '@angular/core'; @Component({ ${componentDecorator} }) export class AppComponent { someMethod() {} } `; } describe('inline templates', () => { const ngUrl = `${componentPath}.AppComponent.html`; function templateDecorator(template: string) { return `template: \`${template}\`,`; } declareTests({ngUrl, templateDecorator}); }); describe('external templates', () => { const ngUrl = '/app/app.component.html'; const templateUrl = '/app/app.component.html'; function templateDecorator(template: string) { appDir['app.component.html'] = template; return `templateUrl: 'app.component.html',`; } declareTests({ngUrl, templateDecorator}); }); function declareTests({ngUrl, templateDecorator}: {ngUrl: string, templateDecorator: (template: string) => string}) { it('should use the right source url in html parse errors', () => { appDir['app.component.ts'] = createComponentSource(templateDecorator('
\n ')); expect(() => compileApp()) .toThrowError(new RegExp(`Template parse errors[\\s\\S]*${ngUrl}@1:2`)); }); it('should use the right source url in template parse errors', () => { appDir['app.component.ts'] = createComponentSource(templateDecorator('
\n
')); expect(() => compileApp()) .toThrowError(new RegExp(`Template parse errors[\\s\\S]*${ngUrl}@1:7`)); }); it('should create a sourceMap for the template', () => { const template = 'Hello World!'; appDir['app.component.ts'] = createComponentSource(templateDecorator(template)); const genFile = compileApp(); const genSource = toTypeScript(genFile); const sourceMap = extractSourceMap(genSource)!; expect(sourceMap.file).toEqual(genFile.genFileUrl); // Note: the generated file also contains code that is not mapped to // the template (e.g. import statements, ...) const templateIndex = sourceMap.sources.indexOf(ngUrl); expect(sourceMap.sourcesContent[templateIndex]).toEqual(template); // for the mapping to the original source file we don't store the source code // as we want to keep whatever TypeScript / ... produced for them. const sourceIndex = sourceMap.sources.indexOf(ngFactoryPath); expect(sourceMap.sourcesContent[sourceIndex]).toBe(' '); }); it('should map elements correctly to the source', () => { const template = '
\n
'; appDir['app.component.ts'] = createComponentSource(templateDecorator(template)); const genFile = compileApp(); const genSource = toTypeScript(genFile); const sourceMap = extractSourceMap(genSource)!; expect(originalPositionFor(sourceMap, findLineAndColumn(genSource, `'span'`))) .toEqual({line: 2, column: 3, source: ngUrl}); }); it('should map bindings correctly to the source', () => { const template = `
\n
`; appDir['app.component.ts'] = createComponentSource(templateDecorator(template)); const genFile = compileApp(); const genSource = toTypeScript(genFile); const sourceMap = extractSourceMap(genSource)!; expect(originalPositionFor(sourceMap, findLineAndColumn(genSource, `someMethod()`))) .toEqual({line: 2, column: 9, source: ngUrl}); }); it('should map events correctly to the source', () => { const template = `
\n
`; appDir['app.component.ts'] = createComponentSource(templateDecorator(template)); const genFile = compileApp(); const genSource = toTypeScript(genFile); const sourceMap = extractSourceMap(genSource)!; expect(originalPositionFor(sourceMap, findLineAndColumn(genSource, `someMethod()`))) .toEqual({line: 2, column: 9, source: ngUrl}); }); it('should map non template parts to the factory file', () => { appDir['app.component.ts'] = createComponentSource(templateDecorator('Hello World!')); const genFile = compileApp(); const genSource = toTypeScript(genFile); const sourceMap = extractSourceMap(genSource)!; expect(originalPositionFor(sourceMap, {line: 1, column: 0})) .toEqual({line: 1, column: 0, source: ngFactoryPath}); }); } }); describe('errors', () => { it('should not error or warn if an unprovided @Injectable with DI-incompatible ' + 'constructor is discovered', () => { const FILES: MockDirectory = { app: { 'app.ts': ` import {Injectable, NgModule} from '@angular/core'; // This injectable is not provided. It is used as a base class for another // service but is not directly provided. It's allowed for such classes to // have a decorator applied as they use Angular features. @Injectable() export class ServiceBase { constructor(a: boolean) {} ngOnDestroy() {} } @Injectable() export class MyService extends ServiceBase { constructor() { super(true); } } @NgModule({providers: [MyService]}) export class AppModule {} ` } }; spyOn(console, 'error'); spyOn(console, 'warn'); expect(() => compile([FILES, angularFiles])).not.toThrowError(); expect(console.warn).toHaveBeenCalledTimes(0); expect(console.error).toHaveBeenCalledTimes(0); }); it('should error if parameters of a provided @Injectable class cannot be resolved', () => { const FILES: MockDirectory = { app: { 'app.ts': ` import {Injectable, NgModule} from '@angular/core'; @Injectable() export class MyService { constructor(a: boolean) {} } @NgModule({ providers: [MyService], }) export class MyModule {} ` } }; expect(() => compile([FILES, angularFiles])) .toThrowError(`Can't resolve all parameters for MyService in /app/app.ts: (?).`); }); it('should error if not all arguments of an @Injectable class can be resolved if strictInjectionParameters is true', () => { const FILES: MockDirectory = { app: { 'app.ts': ` import {Injectable} from '@angular/core'; @Injectable() export class MyService { constructor(a: boolean) {} } ` } }; const warnSpy = spyOn(console, 'warn'); expect(() => compile([FILES, angularFiles], {strictInjectionParameters: true})) .toThrowError(`Can't resolve all parameters for MyService in /app/app.ts: (?).`); expect(warnSpy).not.toHaveBeenCalled(); }); it('should be able to suppress a null access', () => { const FILES: MockDirectory = { app: { 'app.ts': ` import {Component, NgModule} from '@angular/core'; interface Person { name: string; } @Component({ selector: 'my-comp', template: '{{maybe_person!.name}}' }) export class MyComp { maybe_person?: Person; } @NgModule({ declarations: [MyComp] }) export class MyModule {} ` } }; compile([FILES, angularFiles], {postCompile: expectNoDiagnostics}); }); it('should not contain a self import in factory', () => { const FILES: MockDirectory = { app: { 'app.ts': ` import {Component, NgModule} from '@angular/core'; interface Person { name: string; } @Component({ selector: 'my-comp', template: '{{person.name}}' }) export class MyComp { person: Person; } @NgModule({ declarations: [MyComp] }) export class MyModule {} ` } }; compile([FILES, angularFiles], { postCompile: program => { const factorySource = program.getSourceFile('/app/app.ngfactory.ts')!; expect(factorySource.text).not.toContain('\'/app/app.ngfactory\''); } }); }); }); it('should report when a component is declared in any module', () => { const FILES: MockDirectory = { app: { 'app.ts': ` import {Component, NgModule} from '@angular/core'; @Component({selector: 'my-comp', template: ''}) export class MyComp {} @NgModule({}) export class MyModule {} ` } }; expect(() => compile([FILES, angularFiles])) .toThrowError(/Cannot determine the module for class MyComp/); }); it('should add the preamble to generated files', () => { const FILES: MockDirectory = { app: { 'app.ts': ` import { NgModule, Component } from '@angular/core'; @Component({ template: '' }) export class AppComponent {} @NgModule({ declarations: [ AppComponent ] }) export class AppModule { } ` } }; const genFilePreamble = '/* Hello world! */'; const {genFiles} = compile([FILES, angularFiles]); const genFile = genFiles.find(gf => gf.srcFileUrl === '/app/app.ts' && gf.genFileUrl.endsWith('.ts'))!; const genSource = toTypeScript(genFile, genFilePreamble); expect(genSource.startsWith(genFilePreamble)).toBe(true); }); it('should be able to use animation macro methods', () => { const FILES = { app: { 'app.ts': ` import {Component, NgModule} from '@angular/core'; import {trigger, state, style, transition, animate} from '@angular/animations'; export const EXPANSION_PANEL_ANIMATION_TIMING = '225ms cubic-bezier(0.4,0.0,0.2,1)'; @Component({ selector: 'app-component', template: '
', animations: [ trigger('bodyExpansion', [ state('collapsed', style({height: '0px'})), state('expanded', style({height: '*'})), transition('expanded <=> collapsed', animate(EXPANSION_PANEL_ANIMATION_TIMING)), ]), trigger('displayMode', [ state('collapsed', style({margin: '0'})), state('default', style({margin: '16px 0'})), state('flat', style({margin: '0'})), transition('flat <=> collapsed, default <=> collapsed, flat <=> default', animate(EXPANSION_PANEL_ANIMATION_TIMING)), ]), ], }) export class AppComponent { } @NgModule({ declarations: [ AppComponent ] }) export class AppModule { } ` } }; compile([FILES, angularFiles]); }); it('should detect an entry component via an indirection', () => { const FILES = { app: { 'app.ts': ` import {NgModule, ANALYZE_FOR_ENTRY_COMPONENTS} from '@angular/core'; import {AppComponent} from './app.component'; import {COMPONENT_VALUE, MyComponent} from './my-component'; @NgModule({ declarations: [ AppComponent, MyComponent ], bootstrap: [ AppComponent ], providers: [{ provide: ANALYZE_FOR_ENTRY_COMPONENTS, multi: true, useValue: COMPONENT_VALUE }], }) export class AppModule { } `, 'app.component.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'app-component', template: '
', }) export class AppComponent { } `, 'my-component.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-component', template: '
', }) export class MyComponent {} export const COMPONENT_VALUE = [{a: 'b', component: MyComponent}]; ` } }; const result = compile([FILES, angularFiles]); const appModuleFactory = result.genFiles.find(f => /my-component\.ngfactory/.test(f.genFileUrl)); expect(appModuleFactory).toBeDefined(); if (appModuleFactory) { expect(toTypeScript(appModuleFactory)).toContain('MyComponentNgFactory'); } }); describe('ComponentFactories', () => { it('should include inputs, outputs and ng-content selectors in the component factory', () => { const FILES: MockDirectory = { app: { 'app.ts': ` import {Component, NgModule, Input, Output} from '@angular/core'; @Component({ selector: 'my-comp', template: '' + '' + '' + '' + '' }) export class MyComp { @Input('aInputName') aInputProp: string; @Output('aOutputName') aOutputProp: any; } @NgModule({ declarations: [MyComp] }) export class MyModule {} ` } }; const {genFiles} = compile([FILES, angularFiles]); const genFile = genFiles.find(genFile => genFile.srcFileUrl === '/app/app.ts')!; const genSource = toTypeScript(genFile); const createComponentFactoryCall = /ɵccf\([^)]*\)/m.exec(genSource)![0].replace(/\s*/g, ''); // selector expect(createComponentFactoryCall).toContain('my-comp'); // inputs expect(createComponentFactoryCall).toContain(`{aInputProp:'aInputName'}`); // outputs expect(createComponentFactoryCall).toContain(`{aOutputProp:'aOutputName'}`); // ngContentSelectors - note that the catch-all doesn't have to appear at the start expect(createComponentFactoryCall).toContain(`['child1','*','child2','child3','child1']`); }); }); describe('generated templates', () => { it('should not call `check` for directives without bindings nor ngDoCheck/ngOnInit', () => { const FILES: MockDirectory = { app: { 'app.ts': ` import { NgModule, Component } from '@angular/core'; @Component({ template: '' }) export class AppComponent {} @NgModule({ declarations: [ AppComponent ] }) export class AppModule { } ` } }; const {genFiles} = compile([FILES, angularFiles]); const genFile = genFiles.find(gf => gf.srcFileUrl === '/app/app.ts' && gf.genFileUrl.endsWith('.ts'))!; const genSource = toTypeScript(genFile); expect(genSource).not.toContain('check('); }); }); describe('summaries', () => { let angularSummaryFiles: MockDirectory; beforeAll(() => { angularSummaryFiles = compile(angularFiles, {useSummaries: false, emit: true}).outDir; }); inheritanceWithSummariesSpecs(() => angularSummaryFiles); describe('external symbol re-exports enabled', () => { it('should not reexport type symbols mentioned in constructors', () => { const libInput: MockDirectory = { 'lib': { 'base.ts': ` export class AValue {} export type AType = {}; export class AClass { constructor(a: AType, b: AValue) {} } ` } }; const appInput: MockDirectory = { 'app': { 'main.ts': ` export {AClass} from '../lib/base'; ` } }; const {outDir: libOutDir} = compile( [libInput, angularSummaryFiles], {useSummaries: true, createExternalSymbolFactoryReexports: true}); const {genFiles: appGenFiles} = compile( [appInput, libOutDir, angularSummaryFiles], {useSummaries: true, createExternalSymbolFactoryReexports: true}); const appNgFactory = appGenFiles.find((f) => f.genFileUrl === '/app/main.ngfactory.ts')!; const appNgFactoryTs = toTypeScript(appNgFactory); expect(appNgFactoryTs).not.toContain('AType'); expect(appNgFactoryTs).toContain('AValue'); }); it('should not reexport complex function calls', () => { const libInput: MockDirectory = { 'lib': { 'base.ts': ` export class AClass { constructor(arg: any) {} static create(arg: any = null): AClass { return new AClass(arg); } call(arg: any) {} } export function simple(arg: any) { return [arg]; } export const ctor_arg = {}; export const ctor_call = new AClass(ctor_arg); export const static_arg = {}; export const static_call = AClass.create(static_arg); export const complex_arg = {}; export const complex_call = AClass.create().call(complex_arg); export const simple_arg = {}; export const simple_call = simple(simple_arg); ` } }; const appInput: MockDirectory = { 'app': { 'main.ts': ` import {ctor_call, static_call, complex_call, simple_call} from '../lib/base'; export const calls = [ctor_call, static_call, complex_call, simple_call]; `, } }; const {outDir: libOutDir} = compile( [libInput, angularSummaryFiles], {useSummaries: true, createExternalSymbolFactoryReexports: true}); const {genFiles: appGenFiles} = compile( [appInput, libOutDir, angularSummaryFiles], {useSummaries: true, createExternalSymbolFactoryReexports: true}); const appNgFactory = appGenFiles.find((f) => f.genFileUrl === '/app/main.ngfactory.ts')!; const appNgFactoryTs = toTypeScript(appNgFactory); // metadata of ctor calls is preserved, so we reexport the argument expect(appNgFactoryTs).toContain('ctor_arg'); expect(appNgFactoryTs).toContain('ctor_call'); // metadata of static calls is preserved, so we reexport the argument expect(appNgFactoryTs).toContain('static_arg'); expect(appNgFactoryTs).toContain('AClass'); expect(appNgFactoryTs).toContain('static_call'); // metadata of complex calls is elided, so we don't reexport the argument expect(appNgFactoryTs).not.toContain('complex_arg'); expect(appNgFactoryTs).toContain('complex_call'); // metadata of simple calls is preserved, so we reexport the argument expect(appNgFactoryTs).toContain('simple_arg'); expect(appNgFactoryTs).toContain('simple_call'); }); it('should not reexport already exported symbols except for lowered symbols', () => { const libInput: MockDirectory = { 'lib': { 'base.ts': ` export const exportedVar = 1; // A symbol introduced by lowering expressions export const ɵ1 = 'lowered symbol'; ` } }; const appInput: MockDirectory = { 'app': { 'main.ts': `export * from '../lib/base';`, } }; const {outDir: libOutDir} = compile( [libInput, angularSummaryFiles], {useSummaries: true, createExternalSymbolFactoryReexports: true}); const {genFiles: appGenFiles} = compile( [appInput, libOutDir, angularSummaryFiles], {useSummaries: true, createExternalSymbolFactoryReexports: true}); const appNgFactory = appGenFiles.find((f) => f.genFileUrl === '/app/main.ngfactory.ts')!; const appNgFactoryTs = toTypeScript(appNgFactory); // we don't need to reexport exported symbols via the .ngfactory // as we can refer to them via the reexport. expect(appNgFactoryTs).not.toContain('exportedVar'); // although ɵ1 is reexported via `export *`, we still need to reexport it // via the .ngfactory as tsickle expands `export *` into named exports, // and doesn't know about our lowered symbols as we introduce them // after the typecheck phase. expect(appNgFactoryTs).toContain('ɵ1'); }); }); }); function inheritanceWithSummariesSpecs(getAngularSummaryFiles: () => MockDirectory) { function compileParentAndChild( {parentClassDecorator, parentModuleDecorator, childClassDecorator, childModuleDecorator}: { parentClassDecorator: string, parentModuleDecorator: string, childClassDecorator: string, childModuleDecorator: string }) { const libInput: MockDirectory = { 'lib': { 'base.ts': ` import {Injectable, Pipe, Directive, Component, NgModule} from '@angular/core'; ${parentClassDecorator} export class Base {} ${parentModuleDecorator} export class BaseModule {} ` } }; const appInput: MockDirectory = { 'app': { 'main.ts': ` import {Injectable, Pipe, Directive, Component, NgModule} from '@angular/core'; import {Base} from '../lib/base'; ${childClassDecorator} export class Extends extends Base {} ${childModuleDecorator} export class MyModule {} ` } }; const {outDir: libOutDir} = compile([libInput, getAngularSummaryFiles()], {useSummaries: true}); const {genFiles} = compile([libOutDir, appInput, getAngularSummaryFiles()], {useSummaries: true}); return genFiles.find(gf => gf.srcFileUrl === '/app/main.ts'); } it('should inherit ctor and lifecycle hooks from classes in other compilation units', () => { const libInput: MockDirectory = { 'lib': { 'base.ts': ` export class AParam {} export class Base { constructor(a: AParam) {} ngOnDestroy() {} } ` } }; const appInput: MockDirectory = { 'app': { 'main.ts': ` import {NgModule, Component} from '@angular/core'; import {Base} from '../lib/base'; @Component({template: ''}) export class Extends extends Base {} @NgModule({ declarations: [Extends] }) export class MyModule {} ` } }; const {outDir: libOutDir} = compile([libInput, getAngularSummaryFiles()], {useSummaries: true}); const {genFiles} = compile([libOutDir, appInput, getAngularSummaryFiles()], {useSummaries: true}); const mainNgFactory = genFiles.find(gf => gf.srcFileUrl === '/app/main.ts')!; const flags = NodeFlags.TypeDirective | NodeFlags.Component | NodeFlags.OnDestroy; expect(toTypeScript(mainNgFactory)) .toContain(`${flags},(null as any),0,i1.Extends,[i2.AParam]`); }); it('should inherit ctor and lifecycle hooks from classes in other compilation units over 2 levels', () => { const lib1Input: MockDirectory = { 'lib1': { 'base.ts': ` export class AParam {} export class Base { constructor(a: AParam) {} ngOnDestroy() {} } ` } }; const lib2Input: MockDirectory = { 'lib2': { 'middle.ts': ` import {Base} from '../lib1/base'; export class Middle extends Base {} ` } }; const appInput: MockDirectory = { 'app': { 'main.ts': ` import {NgModule, Component} from '@angular/core'; import {Middle} from '../lib2/middle'; @Component({template: ''}) export class Extends extends Middle {} @NgModule({ declarations: [Extends] }) export class MyModule {} ` } }; const {outDir: lib1OutDir} = compile([lib1Input, getAngularSummaryFiles()], {useSummaries: true}); const {outDir: lib2OutDir} = compile([lib1OutDir, lib2Input, getAngularSummaryFiles()], {useSummaries: true}); const {genFiles} = compile( [lib1OutDir, lib2OutDir, appInput, getAngularSummaryFiles()], {useSummaries: true}); const mainNgFactory = genFiles.find(gf => gf.srcFileUrl === '/app/main.ts')!; const flags = NodeFlags.TypeDirective | NodeFlags.Component | NodeFlags.OnDestroy; const mainNgFactorySource = toTypeScript(mainNgFactory); expect(mainNgFactorySource).toContain(`import * as i2 from '/lib1/base';`); expect(mainNgFactorySource).toContain(`${flags},(null as any),0,i1.Extends,[i2.AParam]`); }); describe('Injectable', () => { it('should allow to inherit', () => { const mainNgFactory = compileParentAndChild({ parentClassDecorator: '@Injectable()', parentModuleDecorator: '@NgModule({providers: [Base]})', childClassDecorator: '@Injectable()', childModuleDecorator: '@NgModule({providers: [Extends]})', }); expect(mainNgFactory).toBeTruthy(); }); it('should error if the child class has no matching decorator', () => { expect(() => compileParentAndChild({ parentClassDecorator: '@Injectable()', parentModuleDecorator: '@NgModule({providers: [Base]})', childClassDecorator: '', childModuleDecorator: '@NgModule({providers: [Extends]})', })) .toThrowError(`Error during template compile of 'Extends' Class Extends in /app/main.ts extends from a Injectable in another compilation unit without duplicating the decorator Please add a Injectable or Pipe or Directive or Component or NgModule decorator to the class.`); }); }); describe('Component', () => { it('should allow to inherit', () => { const mainNgFactory = compileParentAndChild({ parentClassDecorator: `@Component({template: ''})`, parentModuleDecorator: '@NgModule({declarations: [Base]})', childClassDecorator: `@Component({template: ''})`, childModuleDecorator: '@NgModule({declarations: [Extends]})' }); expect(mainNgFactory).toBeTruthy(); }); it('should error if the child class has no matching decorator', () => { expect(() => compileParentAndChild({ parentClassDecorator: `@Component({template: ''})`, parentModuleDecorator: '@NgModule({declarations: [Base]})', childClassDecorator: '', childModuleDecorator: '@NgModule({declarations: [Extends]})', })) .toThrowError(`Error during template compile of 'Extends' Class Extends in /app/main.ts extends from a Directive in another compilation unit without duplicating the decorator Please add a Directive or Component decorator to the class.`); }); }); describe('Directive', () => { it('should allow to inherit', () => { const mainNgFactory = compileParentAndChild({ parentClassDecorator: `@Directive({selector: '[someDir]'})`, parentModuleDecorator: '@NgModule({declarations: [Base]})', childClassDecorator: `@Directive({selector: '[someDir]'})`, childModuleDecorator: '@NgModule({declarations: [Extends]})', }); expect(mainNgFactory).toBeTruthy(); }); it('should error if the child class has no matching decorator', () => { expect(() => compileParentAndChild({ parentClassDecorator: `@Directive({selector: '[someDir]'})`, parentModuleDecorator: '@NgModule({declarations: [Base]})', childClassDecorator: '', childModuleDecorator: '@NgModule({declarations: [Extends]})', })) .toThrowError(`Error during template compile of 'Extends' Class Extends in /app/main.ts extends from a Directive in another compilation unit without duplicating the decorator Please add a Directive or Component decorator to the class.`); }); }); describe('Pipe', () => { it('should allow to inherit', () => { const mainNgFactory = compileParentAndChild({ parentClassDecorator: `@Pipe({name: 'somePipe'})`, parentModuleDecorator: '@NgModule({declarations: [Base]})', childClassDecorator: `@Pipe({name: 'somePipe'})`, childModuleDecorator: '@NgModule({declarations: [Extends]})', }); expect(mainNgFactory).toBeTruthy(); }); it('should error if the child class has no matching decorator', () => { expect(() => compileParentAndChild({ parentClassDecorator: `@Pipe({name: 'somePipe'})`, parentModuleDecorator: '@NgModule({declarations: [Base]})', childClassDecorator: '', childModuleDecorator: '@NgModule({declarations: [Extends]})', })) .toThrowError(`Error during template compile of 'Extends' Class Extends in /app/main.ts extends from a Pipe in another compilation unit without duplicating the decorator Please add a Pipe decorator to the class.`); }); }); describe('NgModule', () => { it('should allow to inherit', () => { const mainNgFactory = compileParentAndChild({ parentClassDecorator: `@NgModule()`, parentModuleDecorator: '', childClassDecorator: `@NgModule()`, childModuleDecorator: '', }); expect(mainNgFactory).toBeTruthy(); }); it('should error if the child class has no matching decorator', () => { expect(() => compileParentAndChild({ parentClassDecorator: `@NgModule()`, parentModuleDecorator: '', childClassDecorator: '', childModuleDecorator: '', })) .toThrowError(`Error during template compile of 'Extends' Class Extends in /app/main.ts extends from a NgModule in another compilation unit without duplicating the decorator Please add a NgModule decorator to the class.`); }); }); } }); describe('compiler (bundled Angular)', () => { let angularFiles: Map = setup(); beforeAll(() => { if (!isInBazel()) { // If we are not using Bazel then we need to build these files explicitly const emittingHost = new EmittingCompilerHost(['@angular/core/index'], {emitMetadata: false}); // Create the metadata bundled const indexModule = emittingHost.effectiveName('@angular/core/index'); const bundler = new MetadataBundler( indexModule, '@angular/core', new MockMetadataBundlerHost(emittingHost)); const bundle = bundler.getMetadataBundle(); const metadata = JSON.stringify(bundle.metadata, null, ' '); const bundleIndexSource = privateEntriesToIndex('./index', bundle.privates); emittingHost.override('@angular/core/bundle_index.ts', bundleIndexSource); emittingHost.addWrittenFile( '@angular/core/package.json', JSON.stringify({typings: 'bundle_index.d.ts'})); emittingHost.addWrittenFile('@angular/core/bundle_index.metadata.json', metadata); // Emit the sources const bundleIndexName = emittingHost.effectiveName('@angular/core/bundle_index.ts'); const emittingProgram = ts.createProgram([bundleIndexName], settings, emittingHost); emittingProgram.emit(); angularFiles = emittingHost.writtenAngularFiles(); } }); describe('Quickstart', () => { it('should compile', () => { const {genFiles} = compile([QUICKSTART, angularFiles]); expect(genFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl))).toBeDefined(); expect(genFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl))).toBeDefined(); }); it('should support tsx', () => { const tsOptions = {jsx: ts.JsxEmit.React}; const {genFiles} = compile([QUICKSTART_TSX, angularFiles], /* options */ undefined, tsOptions); expect(genFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl))).toBeDefined(); expect(genFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl))).toBeDefined(); }); }); describe('Bundled library', () => { let libraryFiles: MockDirectory; beforeAll(() => { // Emit the library bundle const emittingHost = new EmittingCompilerHost(['/bolder/index.ts'], {emitMetadata: false, mockData: LIBRARY}); if (isInBazel()) { // In bazel we can just add the angular files from the ones read during setup. emittingHost.addFiles(angularFiles); } // Create the metadata bundled const indexModule = '/bolder/public-api'; const bundler = new MetadataBundler(indexModule, 'bolder', new MockMetadataBundlerHost(emittingHost)); const bundle = bundler.getMetadataBundle(); const metadata = JSON.stringify(bundle.metadata, null, ' '); const bundleIndexSource = privateEntriesToIndex('./public-api', bundle.privates); emittingHost.override('/bolder/index.ts', bundleIndexSource); emittingHost.addWrittenFile('/bolder/index.metadata.json', metadata); // Emit the sources const emittingProgram = ts.createProgram(['/bolder/index.ts'], settings, emittingHost); emittingProgram.emit(); const libFiles = emittingHost.written; // Copy the .html file const htmlFileName = '/bolder/src/bolder.component.html'; libFiles.set(htmlFileName, emittingHost.readFile(htmlFileName)); libraryFiles = arrayToMockDir(toMockFileArray(libFiles).map( ({fileName, content}) => ({fileName: `/node_modules${fileName}`, content}))); }); it('should compile', () => compile([LIBRARY_USING_APP, libraryFiles, angularFiles])); }); }); const QUICKSTART: MockDirectory = { quickstart: { app: { 'app.component.ts': ` import {Component} from '@angular/core'; @Component({ template: '

Hello {{name}}

' }) export class AppComponent { name = 'Angular'; } `, 'app.module.ts': ` import { NgModule } from '@angular/core'; import { toString } from './utils'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], bootstrap: [ AppComponent ] }) export class AppModule { } `, // #15420 'utils.ts': ` export function toString(value: any): string { return ''; } ` } } }; const QUICKSTART_TSX: MockDirectory = { quickstart: { app: { // #20555 'app.component.tsx': ` import {Component} from '@angular/core'; @Component({ template: '

Hello {{name}}

' }) export class AppComponent { name = 'Angular'; } `, 'app.module.ts': ` import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], bootstrap: [ AppComponent ] }) export class AppModule { } ` } } }; const LIBRARY: MockDirectory = { bolder: { 'public-api.ts': ` export * from './src/bolder.component'; export * from './src/bolder.module'; export {BolderModule as ReExportedModule} from './src/bolder.module'; `, src: { 'bolder.component.ts': ` import {Component, Input} from '@angular/core'; @Component({ selector: 'bolder', templateUrl: './bolder.component.html' }) export class BolderComponent { @Input() data: string; } `, 'bolder.component.html': ` {{data}} `, 'bolder.module.ts': ` import {NgModule} from '@angular/core'; import {BolderComponent} from './bolder.component'; @NgModule({ declarations: [BolderComponent], exports: [BolderComponent] }) export class BolderModule {} ` } } }; const LIBRARY_USING_APP: MockDirectory = { 'lib-user': { app: { 'app.component.ts': ` import {Component} from '@angular/core'; @Component({ template: '

Hello

' }) export class AppComponent { name = 'Angular'; } `, 'app.module.ts': ` import { NgModule } from '@angular/core'; import { BolderModule } from 'bolder'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], bootstrap: [ AppComponent ], imports: [ BolderModule ] }) export class AppModule { } ` } } };