https://github.com/angular/angular
Raw File
Tip revision: b7212f5afeb229f1036a68b8e10e7634f5ab694c authored by Chuck Jazdzewski on 17 March 2017, 02:57:49 UTC
docs: add changelog for 4.0.0-rc.4
Tip revision: b7212f5
compiler_spec.ts
/**
 * @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 {AotCompiler, AotCompilerHost, AotCompilerOptions, GeneratedFile, createAotCompiler} from '@angular/compiler';
import {RenderComponentType, ɵReflectionCapabilities as ReflectionCapabilities, ɵreflector as reflector} from '@angular/core';
import {async} from '@angular/core/testing';
import {MetadataBundler, MetadataCollector, ModuleMetadata, privateEntriesToIndex} from '@angular/tsc-wrapped';
import * as ts from 'typescript';

import {extractSourceMap, originalPositionFor} from '../output/source_map_util';

import {EmittingCompilerHost, MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, MockMetadataBundlerHost, settings} from './test_util';

const DTS = /\.d\.ts$/;

const minCoreIndex = `
  export * from './src/application_module';
  export * from './src/change_detection';
  export * from './src/metadata';
  export * from './src/di/metadata';
  export * from './src/di/injector';
  export * from './src/di/injection_token';
  export * from './src/linker';
  export * from './src/render';
  export * from './src/codegen_private_exports';
`;

describe('compiler (unbundled Angular)', () => {
  let angularFiles: Map<string, string>;

  beforeAll(() => {
    const emittingHost = new EmittingCompilerHost([], {emitMetadata: true});
    emittingHost.addScript('@angular/core/index.ts', minCoreIndex);
    const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost);
    emittingProgram.emit();

    angularFiles = emittingHost.written;
  });

  // Restore reflector since AoT compiler will update it with a new static reflector
  afterEach(() => { reflector.updateCapabilities(new ReflectionCapabilities()); });

  describe('Quickstart', () => {
    let host: MockCompilerHost;
    let aotHost: MockAotCompilerHost;

    beforeEach(() => {
      host = new MockCompilerHost(QUICKSTART, FILES, angularFiles);
      aotHost = new MockAotCompilerHost(host);
    });

    it('should compile',
       async(() => compile(host, aotHost, expectNoDiagnostics).then(generatedFiles => {
         expect(generatedFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl)))
             .toBeDefined();
         expect(generatedFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl)))
             .toBeDefined();
       })));

    it('should compile using summaries',
       async(() => summaryCompile(host, aotHost).then(generatedFiles => {
         expect(generatedFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl)))
             .toBeDefined();
         expect(generatedFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl)))
             .toBeDefined();
       })));
  });

  describe('aot source mapping', () => {
    const componentPath = '/app/app.component.ts';
    const ngComponentPath = 'ng:///app/app.component.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(): Promise<GeneratedFile> {
      return new Promise((resolve, reject) => {
        const host = new MockCompilerHost(['/app/app.module.ts'], rootDir, angularFiles);
        const aotHost = new MockAotCompilerHost(host);
        let result: GeneratedFile[];
        let error: Error;
        resolve(compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics)
                    .then(
                        (files) => files.find(
                            genFile => genFile.srcFileUrl === componentPath &&
                                genFile.genFileUrl.endsWith('.ts'))));
      });
    }

    function findLineAndColumn(file: string, token: string): {line: number, column: number} {
      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 = `${ngComponentPath}.AppComponent.html`;

      function templateDecorator(template: string) { return `template: \`${template}\`,`; }

      declareTests({ngUrl, templateDecorator});
    });

    describe('external templates', () => {
      const ngUrl = 'ng:///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', async(() => {
           appDir['app.component.ts'] =
               createComponentSource(templateDecorator('<div>\n  </error>'));

           expectPromiseToThrow(
               compileApp(), new RegExp(`Template parse errors[\\s\\S]*${ngUrl}@1:2`));
         }));

      it('should use the right source url in template parse errors', async(() => {
           appDir['app.component.ts'] = createComponentSource(
               templateDecorator('<div>\n  <div unknown="{{ctxProp}}"></div>'));

           expectPromiseToThrow(
               compileApp(), new RegExp(`Template parse errors[\\s\\S]*${ngUrl}@1:7`));
         }));

      it('should create a sourceMap for the template', async(() => {
           const template = 'Hello World!';

           appDir['app.component.ts'] = createComponentSource(templateDecorator(template));

           compileApp().then((genFile) => {
             const sourceMap = extractSourceMap(genFile.source);
             expect(sourceMap.file).toEqual(genFile.genFileUrl);

             // the generated file contains code that is not mapped to
             // the template but rather to the original source file (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(ngComponentPath);
             expect(sourceMap.sourcesContent[sourceIndex]).toBe(null);
           });
         }));

      it('should map elements correctly to the source', async(() => {
           const template = '<div>\n   <span></span></div>';

           appDir['app.component.ts'] = createComponentSource(templateDecorator(template));

           compileApp().then((genFile) => {
             const sourceMap = extractSourceMap(genFile.source);
             expect(originalPositionFor(sourceMap, findLineAndColumn(genFile.source, `'span'`)))
                 .toEqual({line: 2, column: 3, source: ngUrl});
           });
         }));

      it('should map bindings correctly to the source', async(() => {
           const template = `<div>\n   <span [title]="someMethod()"></span></div>`;

           appDir['app.component.ts'] = createComponentSource(templateDecorator(template));

           compileApp().then((genFile) => {
             const sourceMap = extractSourceMap(genFile.source);
             expect(
                 originalPositionFor(sourceMap, findLineAndColumn(genFile.source, `someMethod()`)))
                 .toEqual({line: 2, column: 9, source: ngUrl});
           });
         }));

      it('should map events correctly to the source', async(() => {
           const template = `<div>\n   <span (click)="someMethod()"></span></div>`;

           appDir['app.component.ts'] = createComponentSource(templateDecorator(template));

           compileApp().then((genFile) => {
             const sourceMap = extractSourceMap(genFile.source);
             expect(
                 originalPositionFor(sourceMap, findLineAndColumn(genFile.source, `someMethod()`)))
                 .toEqual({line: 2, column: 9, source: ngUrl});
           });
         }));

      it('should map non template parts to the source file', async(() => {
           appDir['app.component.ts'] = createComponentSource(templateDecorator('Hello World!'));

           compileApp().then((genFile) => {
             const sourceMap = extractSourceMap(genFile.source);
             expect(originalPositionFor(sourceMap, {line: 1, column: 0}))
                 .toEqual({line: 1, column: 0, source: ngComponentPath});
           });
         }));
    }
  });

  describe('errors', () => {
    it('should only warn if not all arguments of an @Injectable class can be resolved',
       async(() => {
         const FILES: MockData = {
           app: {
             'app.ts': `
                import {Injectable} from '@angular/core';

                @Injectable()
                export class MyService {
                  constructor(a: boolean) {}
                }
              `
           }
         };
         const host = new MockCompilerHost(['/app/app.ts'], FILES, angularFiles);
         const aotHost = new MockAotCompilerHost(host);
         const warnSpy = spyOn(console, 'warn');
         compile(host, aotHost, expectNoDiagnostics).then(() => {
           expect(warnSpy).toHaveBeenCalledWith(
               `Warning: Can't resolve all parameters for MyService in /app/app.ts: (?). This will become an error in Angular v5.x`);
         });

       }));
  });

  it('should add the preamble to generated files', async(() => {
       const FILES: MockData = {
         app: {
           'app.ts': `
              import { NgModule, Component } from '@angular/core';

              @Component({ template: '' })
              export class AppComponent {}

              @NgModule({ declarations: [ AppComponent ] })
              export class AppModule { }
            `
         }
       };
       const host = new MockCompilerHost(['/app/app.ts'], FILES, angularFiles);
       const aotHost = new MockAotCompilerHost(host);
       const genFilePreamble = '/* Hello world! */';
       compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics, {genFilePreamble})
           .then((generatedFiles) => {
             const genFile = generatedFiles.find(
                 gf => gf.srcFileUrl === '/app/app.ts' && gf.genFileUrl.endsWith('.ts'));
             expect(genFile.source.startsWith(genFilePreamble)).toBe(true);
           });

     }));
});

describe('compiler (bundled Angular)', () => {
  let angularFiles: Map<string, string>;

  beforeAll(() => {
    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.written;
  });

  describe('Quickstart', () => {
    let host: MockCompilerHost;
    let aotHost: MockAotCompilerHost;

    beforeEach(() => {
      host = new MockCompilerHost(QUICKSTART, FILES, angularFiles);
      aotHost = new MockAotCompilerHost(host);
    });

    // Restore reflector since AoT compiler will update it with a new static reflector
    afterEach(() => { reflector.updateCapabilities(new ReflectionCapabilities()); });

    it('should compile',
       async(() => compile(host, aotHost, expectNoDiagnostics).then(generatedFiles => {
         expect(generatedFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl)))
             .toBeDefined();
         expect(generatedFiles.find(f => /app\.module\.ngfactory\.ts/.test(f.genFileUrl)))
             .toBeDefined();
       })));
  });
});

function expectNoDiagnostics(program: ts.Program) {
  function fileInfo(diagnostic: ts.Diagnostic): string {
    if (diagnostic.file) {
      return `${diagnostic.file.fileName}(${diagnostic.start}): `;
    }
    return '';
  }

  function chars(len: number, ch: string): string { return new Array(len).fill(ch).join(''); }

  function lineNoOf(offset: number, text: string): number {
    let result = 1;
    for (let i = 0; i < offset; i++) {
      if (text[i] == '\n') result++;
    }
    return result;
  }

  function lineInfo(diagnostic: ts.Diagnostic): string {
    if (diagnostic.file) {
      const start = diagnostic.start;
      let end = diagnostic.start + diagnostic.length;
      const source = diagnostic.file.text;
      let lineStart = start;
      let lineEnd = end;
      while (lineStart > 0 && source[lineStart] != '\n') lineStart--;
      if (lineStart < start) lineStart++;
      while (lineEnd < source.length && source[lineEnd] != '\n') lineEnd++;
      let line = source.substring(lineStart, lineEnd);
      const lineIndex = line.indexOf('/n');
      if (lineIndex > 0) {
        line = line.substr(0, lineIndex);
        end = start + lineIndex;
      }
      const lineNo = lineNoOf(start, source) + ': ';
      return '\n' + lineNo + line + '\n' + chars(start - lineStart + lineNo.length, ' ') +
          chars(end - start, '^');
    }
    return '';
  }

  function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) {
    if (diagnostics && diagnostics.length) {
      throw new Error(
          'Errors from TypeScript:\n' +
          diagnostics.map(d => `${fileInfo(d)}${d.messageText}${lineInfo(d)}`).join(' \n'));
    }
  }
  expectNoDiagnostics(program.getOptionsDiagnostics());
  expectNoDiagnostics(program.getSyntacticDiagnostics());
  expectNoDiagnostics(program.getSemanticDiagnostics());
}

function isDTS(fileName: string): boolean {
  return /\.d\.ts$/.test(fileName);
}

function isSource(fileName: string): boolean {
  return /\.ts$/.test(fileName);
}

function isFactory(fileName: string): boolean {
  return /\.ngfactory\./.test(fileName);
}

function summaryCompile(
    host: MockCompilerHost, aotHost: MockAotCompilerHost,
    preCompile?: (program: ts.Program) => void) {
  // First compile the program to generate the summary files.
  return compile(host, aotHost).then(generatedFiles => {
    // Remove generated files that were not generated from a DTS file
    host.remove(generatedFiles.filter(f => !isDTS(f.srcFileUrl)).map(f => f.genFileUrl));

    // Next compile the program shrowding metadata and only treating .ts files as source.
    aotHost.hideMetadata();
    aotHost.tsFilesOnly();

    return compile(host, aotHost);
  });
}

function compile(
    host: MockCompilerHost, aotHost: AotCompilerHost, preCompile?: (program: ts.Program) => void,
    postCompile: (program: ts.Program) => void = expectNoDiagnostics,
    options: AotCompilerOptions = {}) {
  const scripts = host.scriptNames.slice(0);
  const program = ts.createProgram(scripts, settings, host);
  if (preCompile) preCompile(program);
  const {compiler, reflector} = createAotCompiler(aotHost, options);
  return compiler.compileAll(program.getSourceFiles().map(sf => sf.fileName))
      .then(generatedFiles => {
        generatedFiles.forEach(
            file => isSource(file.genFileUrl) ? host.addScript(file.genFileUrl, file.source) :
                                                host.override(file.genFileUrl, file.source));
        const scripts = host.scriptNames.slice(0);
        const newProgram = ts.createProgram(scripts, settings, host);
        if (postCompile) postCompile(newProgram);
        return generatedFiles;
      });
}

const QUICKSTART = ['/quickstart/app/app.module.ts'];
const FILES: MockData = {
  quickstart: {
    app: {
      'app.component.ts': `
        import {Component} from '@angular/core';

        @Component({
          template: '<h1>Hello {{name}}</h1>'
        })
        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 { }
      `
    }
  }
};

function expectPromiseToThrow(p: Promise<any>, msg: RegExp) {
  p.then(
      () => { throw new Error('Expected to throw'); }, (e) => { expect(e.message).toMatch(msg); });
}
back to top