fix(CompilerCli): assert that all pipes and directives are declared by a module

This commit is contained in:
Victor Berchet 2016-10-25 16:28:22 -07:00
parent 02f1222a8d
commit 7221632228
4 changed files with 133 additions and 131 deletions

View File

@ -11,7 +11,7 @@
* Intended to be used in a build step. * Intended to be used in a build step.
*/ */
import * as compiler from '@angular/compiler'; import * as compiler from '@angular/compiler';
import {Directive, NgModule, ViewEncapsulation} from '@angular/core'; import {ViewEncapsulation} from '@angular/core';
import {AngularCompilerOptions, NgcCliOptions} from '@angular/tsc-wrapped'; import {AngularCompilerOptions, NgcCliOptions} from '@angular/tsc-wrapped';
import * as path from 'path'; import * as path from 'path';
import * as ts from 'typescript'; import * as ts from 'typescript';
@ -35,65 +35,11 @@ const PREAMBLE = `/**
`; `;
export class CodeGeneratorModuleCollector {
constructor(
private staticReflector: StaticReflector, private reflectorHost: StaticReflectorHost,
private program: ts.Program, private options: AngularCompilerOptions) {}
getModuleSymbols(): StaticSymbol[] {
// Compare with false since the default should be true
const skipFileNames =
this.options.generateCodeForLibraries === false ? GENERATED_OR_DTS_FILES : GENERATED_FILES;
const ngModules: StaticSymbol[] = [];
this.program.getSourceFiles()
.filter(sourceFile => !skipFileNames.test(sourceFile.fileName))
.forEach(sourceFile => {
const absSrcPath = this.reflectorHost.getCanonicalFileName(sourceFile.fileName);
const moduleMetadata = this.staticReflector.getModuleMetadata(absSrcPath);
if (!moduleMetadata) {
console.log(`WARNING: no metadata found for ${absSrcPath}`);
return;
}
const metadata = moduleMetadata['metadata'];
if (!metadata) {
return;
}
for (const symbol of Object.keys(metadata)) {
if (metadata[symbol] && metadata[symbol].__symbolic == 'error') {
// Ignore symbols that are only included to record error information.
continue;
}
const staticType = this.reflectorHost.findDeclaration(absSrcPath, symbol, absSrcPath);
const annotations = this.staticReflector.annotations(staticType);
annotations.some((annotation) => {
if (annotation instanceof NgModule) {
ngModules.push(staticType);
return true;
}
});
}
});
return ngModules;
}
}
export class CodeGenerator { export class CodeGenerator {
private moduleCollector: CodeGeneratorModuleCollector;
constructor( constructor(
private options: AngularCompilerOptions, private program: ts.Program, private options: AngularCompilerOptions, private program: ts.Program,
public host: ts.CompilerHost, private staticReflector: StaticReflector, public host: ts.CompilerHost, private staticReflector: StaticReflector,
private compiler: compiler.OfflineCompiler, private reflectorHost: StaticReflectorHost) { private compiler: compiler.OfflineCompiler, private reflectorHost: StaticReflectorHost) {}
this.moduleCollector =
new CodeGeneratorModuleCollector(staticReflector, reflectorHost, program, options);
}
// Write codegen in a directory structure matching the sources. // Write codegen in a directory structure matching the sources.
private calculateEmitPath(filePath: string): string { private calculateEmitPath(filePath: string): string {
@ -118,10 +64,11 @@ export class CodeGenerator {
return path.join(this.options.genDir, relativePath); return path.join(this.options.genDir, relativePath);
} }
codegen(): Promise<any> { codegen(options: {transitiveModules: boolean}): Promise<any> {
const ngModules = this.moduleCollector.getModuleSymbols(); const staticSymbols =
extractProgramSymbols(this.program, this.staticReflector, this.reflectorHost, this.options);
return this.compiler.compileModules(ngModules).then(generatedModules => { return this.compiler.compileModules(staticSymbols, options).then(generatedModules => {
generatedModules.forEach(generatedModule => { generatedModules.forEach(generatedModule => {
const sourceFile = this.program.getSourceFile(generatedModule.fileUrl); const sourceFile = this.program.getSourceFile(generatedModule.fileUrl);
const emitPath = this.calculateEmitPath(generatedModule.moduleUrl); const emitPath = this.calculateEmitPath(generatedModule.moduleUrl);
@ -194,3 +141,41 @@ export class CodeGenerator {
options, program, compilerHost, staticReflector, offlineCompiler, reflectorHost); options, program, compilerHost, staticReflector, offlineCompiler, reflectorHost);
} }
} }
export function extractProgramSymbols(
program: ts.Program, staticReflector: StaticReflector, reflectorHost: StaticReflectorHost,
options: AngularCompilerOptions): StaticSymbol[] {
// Compare with false since the default should be true
const skipFileNames =
options.generateCodeForLibraries === false ? GENERATED_OR_DTS_FILES : GENERATED_FILES;
const staticSymbols: StaticSymbol[] = [];
program.getSourceFiles()
.filter(sourceFile => !skipFileNames.test(sourceFile.fileName))
.forEach(sourceFile => {
const absSrcPath = reflectorHost.getCanonicalFileName(sourceFile.fileName);
const moduleMetadata = staticReflector.getModuleMetadata(absSrcPath);
if (!moduleMetadata) {
console.log(`WARNING: no metadata found for ${absSrcPath}`);
return;
}
const metadata = moduleMetadata['metadata'];
if (!metadata) {
return;
}
for (const symbol of Object.keys(metadata)) {
if (metadata[symbol] && metadata[symbol].__symbolic == 'error') {
// Ignore symbols that are only included to record error information.
continue;
}
staticSymbols.push(reflectorHost.findDeclaration(absSrcPath, symbol, absSrcPath));
}
});
return staticSymbols;
}

View File

@ -9,23 +9,20 @@
/** /**
* Extract i18n messages from source code * Extract i18n messages from source code
*
* TODO(vicb): factorize code with the CodeGenerator
*/ */
// Must be imported first, because angular2 decorators throws on load. // Must be imported first, because angular2 decorators throws on load.
import 'reflect-metadata'; import 'reflect-metadata';
import * as compiler from '@angular/compiler'; import * as compiler from '@angular/compiler';
import {Component, NgModule, ViewEncapsulation} from '@angular/core'; import {ViewEncapsulation} from '@angular/core';
import * as tsc from '@angular/tsc-wrapped'; import * as tsc from '@angular/tsc-wrapped';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ReflectorHost, ReflectorHostContext} from './reflector_host'; import {extractProgramSymbols} from './codegen';
import {ReflectorHost} from './reflector_host';
import {StaticAndDynamicReflectionCapabilities} from './static_reflection_capabilities'; import {StaticAndDynamicReflectionCapabilities} from './static_reflection_capabilities';
import {StaticReflector, StaticSymbol} from './static_reflector'; import {StaticReflector, StaticSymbol} from './static_reflector';
const GENERATED_FILES = /\.ngfactory\.ts$|\.css\.ts$|\.css\.shim\.ts$/;
export class Extractor { export class Extractor {
constructor( constructor(
private options: tsc.AngularCompilerOptions, private program: ts.Program, private options: tsc.AngularCompilerOptions, private program: ts.Program,
@ -34,48 +31,13 @@ export class Extractor {
private metadataResolver: compiler.CompileMetadataResolver, private metadataResolver: compiler.CompileMetadataResolver,
private directiveNormalizer: compiler.DirectiveNormalizer) {} private directiveNormalizer: compiler.DirectiveNormalizer) {}
private readModuleSymbols(absSourcePath: string): StaticSymbol[] {
const moduleMetadata = this.staticReflector.getModuleMetadata(absSourcePath);
const modSymbols: StaticSymbol[] = [];
if (!moduleMetadata) {
console.log(`WARNING: no metadata found for ${absSourcePath}`);
return modSymbols;
}
const metadata = moduleMetadata['metadata'];
const symbols = metadata && Object.keys(metadata);
if (!symbols || !symbols.length) {
return modSymbols;
}
for (const symbol of symbols) {
if (metadata[symbol] && metadata[symbol].__symbolic == 'error') {
// Ignore symbols that are only included to record error information.
continue;
}
const staticType = this.reflectorHost.findDeclaration(absSourcePath, symbol, absSourcePath);
const annotations = this.staticReflector.annotations(staticType);
annotations.some(a => {
if (a instanceof NgModule) {
modSymbols.push(staticType);
return true;
}
});
}
return modSymbols;
}
extract(): Promise<compiler.MessageBundle> { extract(): Promise<compiler.MessageBundle> {
const filePaths = const programSymbols: StaticSymbol[] =
this.program.getSourceFiles().map(sf => sf.fileName).filter(f => !GENERATED_FILES.test(f)); extractProgramSymbols(this.program, this.staticReflector, this.reflectorHost, this.options);
const ngModules: StaticSymbol[] = [];
filePaths.forEach((filePath) => ngModules.push(...this.readModuleSymbols(filePath))); const files =
compiler.analyzeNgModules(programSymbols, {transitiveModules: true}, this.metadataResolver)
const files = compiler.analyzeNgModules(ngModules, this.metadataResolver).files; .files;
const errors: compiler.ParseError[] = []; const errors: compiler.ParseError[] = [];
const filePromises: Promise<any>[] = []; const filePromises: Promise<any>[] = [];

View File

@ -19,7 +19,9 @@ import {CodeGenerator} from './codegen';
function codegen( function codegen(
ngOptions: tsc.AngularCompilerOptions, cliOptions: tsc.NgcCliOptions, program: ts.Program, ngOptions: tsc.AngularCompilerOptions, cliOptions: tsc.NgcCliOptions, program: ts.Program,
host: ts.CompilerHost) { host: ts.CompilerHost) {
return CodeGenerator.create(ngOptions, cliOptions, program, host).codegen(); return CodeGenerator.create(ngOptions, cliOptions, program, host).codegen({
transitiveModules: true
});
} }
// CLI entry point // CLI entry point

View File

@ -29,56 +29,81 @@ export class SourceModule {
// Returns all the source files and a mapping from modules to directives // Returns all the source files and a mapping from modules to directives
export function analyzeNgModules( export function analyzeNgModules(
ngModules: StaticSymbol[], metadataResolver: CompileMetadataResolver): { programStaticSymbols: StaticSymbol[], options: {transitiveModules: boolean},
ngModuleByDirective: Map<StaticSymbol, CompileNgModuleMetadata>, metadataResolver: CompileMetadataResolver): {
ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>,
files: Array<{srcUrl: string, directives: StaticSymbol[], ngModules: StaticSymbol[]}> files: Array<{srcUrl: string, directives: StaticSymbol[], ngModules: StaticSymbol[]}>
} { } {
const {
ngModules: programNgModules,
pipesAndDirectives: programPipesOrDirectives,
} = _extractModulesAndPipesOrDirectives(programStaticSymbols, metadataResolver);
const moduleMetasByRef = new Map<any, CompileNgModuleMetadata>(); const moduleMetasByRef = new Map<any, CompileNgModuleMetadata>();
programNgModules.forEach(modMeta => {
if (options.transitiveModules) {
// For every input modules add the list of transitively included modules // For every input modules add the list of transitively included modules
ngModules.forEach(ngModule => {
const modMeta = metadataResolver.getNgModuleMetadata(ngModule);
modMeta.transitiveModule.modules.forEach( modMeta.transitiveModule.modules.forEach(
modMeta => { moduleMetasByRef.set(modMeta.type.reference, modMeta); }); modMeta => { moduleMetasByRef.set(modMeta.type.reference, modMeta); });
} else {
moduleMetasByRef.set(modMeta.type.reference, modMeta);
}
}); });
const ngModuleMetas = MapWrapper.values(moduleMetasByRef); const ngModuleMetas = MapWrapper.values(moduleMetasByRef);
const ngModuleByDirective = new Map<StaticSymbol, CompileNgModuleMetadata>(); const ngModuleByPipeOrDirective = new Map<StaticSymbol, CompileNgModuleMetadata>();
const ngModulesByFile = new Map<string, StaticSymbol[]>(); const ngModulesByFile = new Map<string, StaticSymbol[]>();
const ngDirectivesByFile = new Map<string, StaticSymbol[]>(); const ngDirectivesByFile = new Map<string, StaticSymbol[]>();
const srcFileUrls = new Set<string>(); const filePaths = new Set<string>();
// Looping over all modules to construct: // Looping over all modules to construct:
// - a map from files to modules `ngModulesByFile`, // - a map from file to modules `ngModulesByFile`,
// - a map from files to directives `ngDirectivesByFile`, // - a map from file to directives `ngDirectivesByFile`,
// - a map from modules to directives `ngModuleByDirective`. // - a map from directive/pipe to module `ngModuleByPipeOrDirective`.
ngModuleMetas.forEach((ngModuleMeta) => { ngModuleMetas.forEach((ngModuleMeta) => {
const srcFileUrl = ngModuleMeta.type.reference.filePath; const srcFileUrl = ngModuleMeta.type.reference.filePath;
srcFileUrls.add(srcFileUrl); filePaths.add(srcFileUrl);
ngModulesByFile.set( ngModulesByFile.set(
srcFileUrl, (ngModulesByFile.get(srcFileUrl) || []).concat(ngModuleMeta.type.reference)); srcFileUrl, (ngModulesByFile.get(srcFileUrl) || []).concat(ngModuleMeta.type.reference));
ngModuleMeta.declaredDirectives.forEach((dirMeta: CompileDirectiveMetadata) => { ngModuleMeta.declaredDirectives.forEach((dirMeta: CompileDirectiveMetadata) => {
const fileUrl = dirMeta.type.reference.filePath; const fileUrl = dirMeta.type.reference.filePath;
srcFileUrls.add(fileUrl); filePaths.add(fileUrl);
ngDirectivesByFile.set( ngDirectivesByFile.set(
fileUrl, (ngDirectivesByFile.get(fileUrl) || []).concat(dirMeta.type.reference)); fileUrl, (ngDirectivesByFile.get(fileUrl) || []).concat(dirMeta.type.reference));
ngModuleByDirective.set(dirMeta.type.reference, ngModuleMeta); ngModuleByPipeOrDirective.set(dirMeta.type.reference, ngModuleMeta);
});
ngModuleMeta.declaredPipes.forEach((pipeMeta: CompilePipeMetadata) => {
const fileUrl = pipeMeta.type.reference.filePath;
filePaths.add(fileUrl);
ngModuleByPipeOrDirective.set(pipeMeta.type.reference, ngModuleMeta);
}); });
}); });
// Throw an error if any of the program pipe or directives is not declared by a module
const symbolsMissingModule =
programPipesOrDirectives.filter(s => !ngModuleByPipeOrDirective.has(s));
if (symbolsMissingModule.length) {
const messages = symbolsMissingModule.map(
s => `Cannot determine the module for class ${s.name} in ${s.filePath}!`);
throw new Error(messages.join('\n'));
}
const files: {srcUrl: string, directives: StaticSymbol[], ngModules: StaticSymbol[]}[] = []; const files: {srcUrl: string, directives: StaticSymbol[], ngModules: StaticSymbol[]}[] = [];
srcFileUrls.forEach((srcUrl) => { filePaths.forEach((srcUrl) => {
const directives = ngDirectivesByFile.get(srcUrl) || []; const directives = ngDirectivesByFile.get(srcUrl) || [];
const ngModules = ngModulesByFile.get(srcUrl) || []; const ngModules = ngModulesByFile.get(srcUrl) || [];
files.push({srcUrl, directives, ngModules}); files.push({srcUrl, directives, ngModules});
}); });
return { return {
// map from modules to declared directives // map directive/pipe to module
ngModuleByDirective, ngModuleByPipeOrDirective,
// list of modules and directives for every source file // list modules and directives for every source file
files, files,
}; };
} }
@ -100,19 +125,21 @@ export class OfflineCompiler {
this._metadataResolver.clearCache(); this._metadataResolver.clearCache();
} }
compileModules(ngModules: StaticSymbol[]): Promise<SourceModule[]> { compileModules(staticSymbols: StaticSymbol[], options: {transitiveModules: boolean}):
const {ngModuleByDirective, files} = analyzeNgModules(ngModules, this._metadataResolver); Promise<SourceModule[]> {
const {ngModuleByPipeOrDirective, files} =
analyzeNgModules(staticSymbols, options, this._metadataResolver);
const sourceModules = files.map( const sourceModules = files.map(
file => this._compileSrcFile( file => this._compileSrcFile(
file.srcUrl, ngModuleByDirective, file.directives, file.ngModules)); file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.ngModules));
return Promise.all(sourceModules) return Promise.all(sourceModules)
.then((modules: SourceModule[][]) => ListWrapper.flatten(modules)); .then((modules: SourceModule[][]) => ListWrapper.flatten(modules));
} }
private _compileSrcFile( private _compileSrcFile(
srcFileUrl: string, ngModuleByDirective: Map<StaticSymbol, CompileNgModuleMetadata>, srcFileUrl: string, ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>,
directives: StaticSymbol[], ngModules: StaticSymbol[]): Promise<SourceModule[]> { directives: StaticSymbol[], ngModules: StaticSymbol[]): Promise<SourceModule[]> {
const fileSuffix = _splitTypescriptSuffix(srcFileUrl)[1]; const fileSuffix = _splitTypescriptSuffix(srcFileUrl)[1];
const statements: o.Statement[] = []; const statements: o.Statement[] = [];
@ -134,9 +161,10 @@ export class OfflineCompiler {
if (!compMeta.isComponent) { if (!compMeta.isComponent) {
return Promise.resolve(null); return Promise.resolve(null);
} }
const ngModule = ngModuleByDirective.get(dirType); const ngModule = ngModuleByPipeOrDirective.get(dirType);
if (!ngModule) { if (!ngModule) {
throw new Error(`Cannot determine the module for component ${compMeta.type.name}!`); throw new Error(
`Internal Error: cannot determine the module for component ${compMeta.type.name}!`);
} }
return Promise return Promise
@ -330,3 +358,28 @@ function _splitTypescriptSuffix(path: string): string[] {
return [path, '']; return [path, ''];
} }
// Group the symbols by types:
// - NgModules,
// - Pipes and Directives.
function _extractModulesAndPipesOrDirectives(
programStaticSymbols: StaticSymbol[], metadataResolver: CompileMetadataResolver) {
const ngModules: CompileNgModuleMetadata[] = [];
const pipesAndDirectives: StaticSymbol[] = [];
programStaticSymbols.forEach(staticSymbol => {
const ngModule = metadataResolver.getNgModuleMetadata(staticSymbol, false);
const directive = metadataResolver.getDirectiveMetadata(staticSymbol, false);
const pipe = metadataResolver.getPipeMetadata(<any>staticSymbol, false);
if (ngModule) {
ngModules.push(ngModule);
} else if (directive) {
pipesAndDirectives.push(staticSymbol);
} else if (pipe) {
pipesAndDirectives.push(staticSymbol);
}
});
return {ngModules, pipesAndDirectives};
}