refactor(compiler): Reintroduce `ReflectorHost` and move `Extractor` into `@angular/compiler`

This commit is contained in:
Tobias Bosch 2016-11-18 08:40:41 -08:00 committed by Chuck Jazdzewski
parent 3c06a5dc25
commit 2a5bd2f345
9 changed files with 173 additions and 130 deletions

View File

@ -36,8 +36,8 @@ const PREAMBLE = `/**
export class CodeGenerator {
constructor(
private options: AngularCompilerOptions, private program: ts.Program,
public host: ts.CompilerHost, private staticReflector: compiler.StaticReflector,
private compiler: compiler.AotCompiler, private ngCompilerHost: CompilerHost) {}
public host: ts.CompilerHost, private compiler: compiler.AotCompiler,
private ngCompilerHost: CompilerHost) {}
// Write codegen in a directory structure matching the sources.
private calculateEmitPath(filePath: string): string {
@ -96,7 +96,7 @@ export class CodeGenerator {
}
transContent = readFileSync(transFile, 'utf8');
}
const {compiler: aotCompiler, reflector} = compiler.createAotCompiler(ngCompilerHost, {
const {compiler: aotCompiler} = compiler.createAotCompiler(ngCompilerHost, {
debug: options.debug === true,
translations: transContent,
i18nFormat: cliOptions.i18nFormat,
@ -104,18 +104,10 @@ export class CodeGenerator {
excludeFilePattern: options.generateCodeForLibraries === false ? GENERATED_OR_DTS_FILES :
GENERATED_FILES
});
return new CodeGenerator(
options, program, tsCompilerHost, reflector, aotCompiler, ngCompilerHost);
return new CodeGenerator(options, program, tsCompilerHost, aotCompiler, ngCompilerHost);
}
}
export function extractProgramSymbols(
program: ts.Program, staticReflector: compiler.StaticReflector, compilerHost: CompilerHost,
options: AngularCompilerOptions): compiler.StaticSymbol[] {
return compiler.extractProgramSymbols(
staticReflector,
program.getSourceFiles().map(sf => compilerHost.getCanonicalFileName(sf.fileName)), {
excludeFilePattern: options.generateCodeForLibraries === false ? GENERATED_OR_DTS_FILES :
GENERATED_FILES
});
export function excludeFilePattern(options: AngularCompilerOptions): RegExp {
return options.generateCodeForLibraries === false ? GENERATED_OR_DTS_FILES : GENERATED_FILES;
}

View File

@ -24,17 +24,7 @@ import {Extractor} from './extractor';
function extract(
ngOptions: tsc.AngularCompilerOptions, cliOptions: tsc.I18nExtractionCliOptions,
program: ts.Program, host: ts.CompilerHost) {
const resourceLoader: compiler.ResourceLoader = {
get: (s: string) => {
if (!host.fileExists(s)) {
// TODO: We should really have a test for error cases like this!
throw new Error(`Compilation failed. Resource file not found: ${s}`);
}
return Promise.resolve(host.readFile(s));
}
};
const extractor =
Extractor.create(ngOptions, cliOptions.i18nFormat, program, host, resourceLoader);
const extractor = Extractor.create(ngOptions, cliOptions.i18nFormat, program, host);
const bundlePromise: Promise<compiler.MessageBundle> = extractor.extract();

View File

@ -14,84 +14,28 @@
import 'reflect-metadata';
import * as compiler from '@angular/compiler';
import {ViewEncapsulation} from '@angular/core';
import * as tsc from '@angular/tsc-wrapped';
import * as ts from 'typescript';
import {extractProgramSymbols} from './codegen';
import {excludeFilePattern} from './codegen';
import {CompilerHost} from './compiler_host';
export class Extractor {
constructor(
private options: tsc.AngularCompilerOptions, private program: ts.Program,
public host: ts.CompilerHost, private staticReflector: compiler.StaticReflector,
private messageBundle: compiler.MessageBundle, private compilerHost: CompilerHost,
private metadataResolver: compiler.CompileMetadataResolver) {}
private ngExtractor: compiler.Extractor, private ngCompilerHost: CompilerHost,
private program: ts.Program) {}
extract(): Promise<compiler.MessageBundle> {
const programSymbols: compiler.StaticSymbol[] =
extractProgramSymbols(this.program, this.staticReflector, this.compilerHost, this.options);
const {ngModules, files} =
compiler.analyzeAndValidateNgModules(programSymbols, {}, this.metadataResolver);
return compiler.loadNgModuleDirectives(ngModules).then(() => {
const errors: compiler.ParseError[] = [];
files.forEach(file => {
const compMetas: compiler.CompileDirectiveMetadata[] = [];
file.directives.forEach(directiveType => {
const dirMeta = this.metadataResolver.getDirectiveMetadata(directiveType);
if (dirMeta && dirMeta.isComponent) {
compMetas.push(dirMeta);
}
});
compMetas.forEach(compMeta => {
const html = compMeta.template.template;
const interpolationConfig =
compiler.InterpolationConfig.fromArray(compMeta.template.interpolation);
errors.push(
...this.messageBundle.updateFromTemplate(html, file.srcUrl, interpolationConfig));
});
});
if (errors.length) {
throw new Error(errors.map(e => e.toString()).join('\n'));
}
return this.messageBundle;
});
return this.ngExtractor.extract(this.program.getSourceFiles().map(
sf => this.ngCompilerHost.getCanonicalFileName(sf.fileName)))
}
static create(
options: tsc.AngularCompilerOptions, translationsFormat: string, program: ts.Program,
tsCompilerHost: ts.CompilerHost, resourceLoader: compiler.ResourceLoader,
ngCompilerHost?: CompilerHost): Extractor {
const htmlParser = new compiler.I18NHtmlParser(new compiler.HtmlParser());
const urlResolver: compiler.UrlResolver = compiler.createOfflineCompileUrlResolver();
tsCompilerHost: ts.CompilerHost, ngCompilerHost?: CompilerHost): Extractor {
if (!ngCompilerHost) ngCompilerHost = new CompilerHost(program, tsCompilerHost, options);
const staticReflector = new compiler.StaticReflector(ngCompilerHost);
compiler.StaticAndDynamicReflectionCapabilities.install(staticReflector);
const config = new compiler.CompilerConfig({
genDebugInfo: options.debug === true,
defaultEncapsulation: ViewEncapsulation.Emulated,
logBindingUpdate: false,
useJit: false
});
const normalizer =
new compiler.DirectiveNormalizer(resourceLoader, urlResolver, htmlParser, config);
const elementSchemaRegistry = new compiler.DomElementSchemaRegistry();
const resolver = new compiler.CompileMetadataResolver(
new compiler.NgModuleResolver(staticReflector),
new compiler.DirectiveResolver(staticReflector), new compiler.PipeResolver(staticReflector),
elementSchemaRegistry, normalizer, staticReflector);
// TODO(vicb): implicit tags & attributes
const messageBundle = new compiler.MessageBundle(htmlParser, [], {});
return new Extractor(
options, program, tsCompilerHost, staticReflector, messageBundle, ngCompilerHost, resolver);
const {extractor: ngExtractor} = compiler.Extractor.create(
ngCompilerHost, {excludeFilePattern: excludeFilePattern(options)});
return new Extractor(ngExtractor, ngCompilerHost, program);
}
}
}

View File

@ -6,37 +6,17 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ImportResolver} from '../output/path_util';
import {StaticReflectorHost} from './static_reflector';
import {StaticSymbol} from './static_symbol';
/**
* The host of the AotCompiler disconnects the implementation from TypeScript / other language
* services and from underlying file systems.
*/
export interface AotCompilerHost {
/**
* Return a ModuleMetadata for the given module.
* Angular 2 CLI will produce this metadata for a module whenever a .d.ts files is
* produced and the module has exported variables or classes with decorators. Module metadata can
* also be produced directly from TypeScript sources by using MetadataCollector in tools/metadata.
*
* @param modulePath is a string identifier for a module as an absolute path.
* @returns the metadata for the given module.
*/
getMetadataFor(modulePath: string): {[key: string]: any}[];
/**
* Converts a module name that is used in an `import` to a file path.
* I.e.
* `path/to/containingFile.ts` containing `import {...} from 'module-name'`.
*/
moduleNameToFileName(moduleName: string, containingFile: string): string;
/**
* Converts a file path to a module name that can be used as an `import.
* I.e. `path/to/importedFile.ts` should be imported by `path/to/containingFile.ts`.
*/
fileNameToModuleName(importedFile: string, containingFile: string): string;
export interface AotCompilerHost extends StaticReflectorHost, ImportResolver {
/**
* Loads a resource (e.g. html / css)
*/

View File

@ -8,7 +8,6 @@
import {Attribute, Component, ContentChild, ContentChildren, Directive, Host, HostBinding, HostListener, Inject, Injectable, Input, NgModule, Optional, Output, Pipe, Self, SkipSelf, ViewChild, ViewChildren, animate, group, keyframes, sequence, state, style, transition, trigger} from '@angular/core';
import {ReflectorReader} from '../private_import_core';
import {AotCompilerHost} from './compiler_host';
import {StaticSymbol} from './static_symbol';
const SUPPORTED_SCHEMA_VERSION = 2;
@ -21,6 +20,30 @@ const ANGULAR_IMPORT_LOCATIONS = {
provider: '@angular/core/src/di/provider'
};
/**
* The host of the StaticReflector disconnects the implementation from TypeScript / other language
* services and from underlying file systems.
*/
export interface StaticReflectorHost {
/**
* Return a ModuleMetadata for the given module.
* Angular 2 CLI will produce this metadata for a module whenever a .d.ts files is
* produced and the module has exported variables or classes with decorators. Module metadata can
* also be produced directly from TypeScript sources by using MetadataCollector in tools/metadata.
*
* @param modulePath is a string identifier for a module as an absolute path.
* @returns the metadata for the given module.
*/
getMetadataFor(modulePath: string): {[key: string]: any}[];
/**
* Converts a module name that is used in an `import` to a file path.
* I.e.
* `path/to/containingFile.ts` containing `import {...} from 'module-name'`.
*/
moduleNameToFileName(moduleName: string, containingFile: string): string;
}
/**
* A static reflector implements enough of the Reflector API that is necessary to compile
* templates statically.
@ -35,7 +58,7 @@ export class StaticReflector implements ReflectorReader {
private conversionMap = new Map<StaticSymbol, (context: StaticSymbol, args: any[]) => any>();
private opaqueToken: StaticSymbol;
constructor(private host: AotCompilerHost) { this.initializeConversionMap(); }
constructor(private host: StaticReflectorHost) { this.initializeConversionMap(); }
importUri(typeOrFunc: StaticSymbol): string {
const staticSymbol = this.findDeclaration(typeOrFunc.filePath, typeOrFunc.name, '');

View File

@ -0,0 +1,117 @@
/**
* @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
*/
/**
* Extract i18n messages from source code
*/
import {ViewEncapsulation} from '@angular/core';
import {analyzeAndValidateNgModules, extractProgramSymbols, loadNgModuleDirectives} from '../aot/compiler';
import {StaticAndDynamicReflectionCapabilities} from '../aot/static_reflection_capabilities';
import {StaticReflector, StaticReflectorHost} from '../aot/static_reflector';
import {CompileDirectiveMetadata} from '../compile_metadata';
import {CompilerConfig} from '../config';
import {DirectiveNormalizer} from '../directive_normalizer';
import {DirectiveResolver} from '../directive_resolver';
import {CompileMetadataResolver} from '../metadata_resolver';
import {HtmlParser} from '../ml_parser/html_parser';
import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {NgModuleResolver} from '../ng_module_resolver';
import {ParseError} from '../parse_util';
import {PipeResolver} from '../pipe_resolver';
import {Console} from '../private_import_core';
import {DomElementSchemaRegistry} from '../schema/dom_element_schema_registry';
import {createOfflineCompileUrlResolver} from '../url_resolver';
import {I18NHtmlParser} from './i18n_html_parser';
import {MessageBundle} from './message_bundle';
export interface ExtractorOptions {
includeFilePattern?: RegExp;
excludeFilePattern?: RegExp;
}
/**
* The host of the Extractor disconnects the implementation from TypeScript / other language
* services and from underlying file systems.
*/
export interface ExtractorHost extends StaticReflectorHost {
/**
* Loads a resource (e.g. html / css)
*/
loadResource(path: string): Promise<string>;
}
export class Extractor {
constructor(
private options: ExtractorOptions, public host: ExtractorHost,
private staticReflector: StaticReflector, private messageBundle: MessageBundle,
private metadataResolver: CompileMetadataResolver) {}
extract(rootFiles: string[]): Promise<MessageBundle> {
const programSymbols = extractProgramSymbols(this.staticReflector, rootFiles, this.options);
const {ngModuleByPipeOrDirective, files, ngModules} =
analyzeAndValidateNgModules(programSymbols, this.options, this.metadataResolver);
return loadNgModuleDirectives(ngModules).then(() => {
const errors: ParseError[] = [];
files.forEach(file => {
const compMetas: CompileDirectiveMetadata[] = [];
file.directives.forEach(directiveType => {
const dirMeta = this.metadataResolver.getDirectiveMetadata(directiveType);
if (dirMeta && dirMeta.isComponent) {
compMetas.push(dirMeta);
}
});
compMetas.forEach(compMeta => {
const html = compMeta.template.template;
const interpolationConfig =
InterpolationConfig.fromArray(compMeta.template.interpolation);
errors.push(
...this.messageBundle.updateFromTemplate(html, file.srcUrl, interpolationConfig));
});
});
if (errors.length) {
throw new Error(errors.map(e => e.toString()).join('\n'));
}
return this.messageBundle;
});
}
static create(host: ExtractorHost, options: ExtractorOptions):
{extractor: Extractor, staticReflector: StaticReflector} {
const htmlParser = new I18NHtmlParser(new HtmlParser());
const urlResolver = createOfflineCompileUrlResolver();
const staticReflector = new StaticReflector(host);
StaticAndDynamicReflectionCapabilities.install(staticReflector);
const config = new CompilerConfig({
genDebugInfo: false,
defaultEncapsulation: ViewEncapsulation.Emulated,
logBindingUpdate: false,
useJit: false
});
const normalizer = new DirectiveNormalizer(
{get: (url: string) => host.loadResource(url)}, urlResolver, htmlParser, config);
const elementSchemaRegistry = new DomElementSchemaRegistry();
const resolver = new CompileMetadataResolver(
new NgModuleResolver(staticReflector), new DirectiveResolver(staticReflector),
new PipeResolver(staticReflector), elementSchemaRegistry, normalizer, staticReflector);
// TODO(vicb): implicit tags & attributes
const messageBundle = new MessageBundle(htmlParser, [], {});
const extractor = new Extractor(options, host, staticReflector, messageBundle, resolver);
return {extractor, staticReflector};
}
}

View File

@ -12,3 +12,4 @@ export {Serializer} from './serializers/serializer';
export {Xliff} from './serializers/xliff';
export {Xmb} from './serializers/xmb';
export {Xtb} from './serializers/xtb';
export * from './extractor';

View File

@ -10,5 +10,9 @@
* Interface that defines how import statements should be generated.
*/
export abstract class ImportResolver {
/**
* Converts a file path to a module name that can be used as an `import.
* I.e. `path/to/importedFile.ts` should be imported by `path/to/containingFile.ts`.
*/
abstract fileNameToModuleName(importedFilePath: string, containingFilePath: string): string;
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotCompilerHost, StaticReflector, StaticSymbol} from '@angular/compiler';
import {StaticReflector, StaticReflectorHost, StaticSymbol} from '@angular/compiler';
import {HostListener, Inject, animate, group, keyframes, sequence, state, style, transition, trigger} from '@angular/core';
import {MetadataCollector} from '@angular/tsc-wrapped';
import * as ts from 'typescript';
@ -18,11 +18,11 @@ const TS_EXT = /(^.|(?!\.d)..)\.ts$/;
describe('StaticReflector', () => {
const noContext = new StaticSymbol('', '');
let host: AotCompilerHost;
let host: StaticReflectorHost;
let reflector: StaticReflector;
beforeEach(() => {
host = new MockAotCompilerHost();
host = new MockStaticReflectorHost();
reflector = new StaticReflector(host);
});
@ -519,17 +519,9 @@ describe('StaticReflector', () => {
});
class MockAotCompilerHost implements AotCompilerHost {
class MockStaticReflectorHost implements StaticReflectorHost {
private collector = new MetadataCollector();
constructor() {}
loadResource(filePath: string): Promise<string> { throw new Error('Should not be called!'); }
fileNameToModuleName(importedFilePath: string, containingFilePath: string): string {
throw new Error('Should not be called!');
}
// In tests, assume that symbols are not re-exported
moduleNameToFileName(modulePath: string, containingFile?: string): string {
function splitPath(path: string): string[] { return path.split(/\/|\\/g); }