feat(compiler): read and write `.ngsummary.json` files

When compiling libraries, this feature extracts the minimal information
from the directives/pipes/modules of the library into `.ngsummary.json` files,
so that applications that use this library only need to be recompiled
if one of the summary files change, but not on every change
of the libraries (e.g. one of the templates).

Only works if individual codegen for libraries is enabled,
see the `generateCodeForLibraries: false` option.

Closes #12787
This commit is contained in:
Tobias Bosch 2016-11-29 15:36:33 -08:00 committed by Alex Rickabaugh
parent 9ab401f4d3
commit 614a35d539
23 changed files with 500 additions and 126 deletions

View File

@ -11,7 +11,9 @@ import {FormsModule} from '@angular/forms';
import {ServerModule} from '@angular/platform-server';
import {MdButtonModule} from '@angular2-material/button';
import {ThirdpartyModule} from '../third_party_src/module';
// Note: don't refer to third_party_src as we want to test that
// we can compile components from node_modules!
import {ThirdpartyModule} from 'third_party/module';
import {MultipleComponentsMyComp, NextComp} from './a/multiple_components';
import {AnimateCmp} from './animate';

View File

@ -0,0 +1,100 @@
#!/usr/bin/env node
/**
* @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
*/
/* tslint:disable:no-console */
// Must be imported first, because angular2 decorators throws on load.
import 'reflect-metadata';
import * as path from 'path';
import * as ts from 'typescript';
import * as assert from 'assert';
import {tsc} from '@angular/tsc-wrapped/src/tsc';
import {AngularCompilerOptions, CodeGenerator, CompilerHostContext, NodeCompilerHostContext} from '@angular/compiler-cli';
/**
* Main method.
* Standalone program that executes the real codegen and tests that
* ngsummary.json files are used for libraries.
*/
function main() {
console.log(`testing usage of ngsummary.json files in libraries...`);
const basePath = path.resolve(__dirname, '..');
const project = path.resolve(basePath, 'tsconfig-build.json');
const readFiles: string[] = [];
class AssertingHostContext extends NodeCompilerHostContext {
readFile(fileName: string): string {
readFiles.push(path.relative(basePath, fileName));
return super.readFile(fileName);
}
readResource(s: string): Promise<string> {
readFiles.push(path.relative(basePath, s));
return super.readResource(s);
}
}
const config = tsc.readConfiguration(project, basePath);
config.ngOptions.basePath = basePath;
// This flag tells ngc do not recompile libraries.
config.ngOptions.generateCodeForLibraries = false;
console.log(`>>> running codegen for ${project}`);
codegen(config, (host) => new AssertingHostContext())
.then((exitCode: any) => {
console.log(`>>> codegen done, asserting read files`);
assertSomeFileMatch(readFiles, /^node_modules\/.*\.ngsummary\.json$/);
assertNoFileMatch(readFiles, /^node_modules\/.*\.html$/);
assertNoFileMatch(readFiles, /^node_modules\/.*\.css$/);
assertNoFileMatch(readFiles, /^src\/.*\.ngsummary\.json$/);
assertSomeFileMatch(readFiles, /^src\/.*\.html$/);
assertSomeFileMatch(readFiles, /^src\/.*\.css$/);
console.log(`done, no errors.`);
process.exit(exitCode);
})
.catch((e: any) => {
console.error(e.stack);
console.error('Compilation failed');
process.exit(1);
});
}
/**
* Simple adaption of tsc-wrapped main to just run codegen with a CompilerHostContext
*/
function codegen(
config: {parsed: ts.ParsedCommandLine, ngOptions: AngularCompilerOptions},
hostContextFactory: (host: ts.CompilerHost) => CompilerHostContext) {
const host = ts.createCompilerHost(config.parsed.options, true);
// HACK: patch the realpath to solve symlink issue here:
// https://github.com/Microsoft/TypeScript/issues/9552
// todo(misko): remove once facade symlinks are removed
host.realpath = (path) => path;
const program = ts.createProgram(config.parsed.fileNames, config.parsed.options, host);
return CodeGenerator.create(config.ngOptions, {
} as any, program, host, hostContextFactory(host)).codegen();
}
function assertSomeFileMatch(fileNames: string[], pattern: RegExp) {
assert(
fileNames.some(fileName => pattern.test(fileName)),
`Expected some read files match ${pattern}`);
}
function assertNoFileMatch(fileNames: string[], pattern: RegExp) {
const matches = fileNames.filter(fileName => pattern.test(fileName));
assert(
matches.length === 0,
`Expected no read files match ${pattern}, but found: \n${matches.join('\n')}`);
}
main();

View File

@ -21,6 +21,7 @@
"src/module",
"src/bootstrap",
"test/all_spec",
"test/test_summaries",
"benchmarks/src/tree/ng2/index_aot.ts",
"benchmarks/src/tree/ng2_switch/index_aot.ts",
"benchmarks/src/largetable/ng2/index_aot.ts",

View File

@ -22,6 +22,7 @@ import {PathMappedCompilerHost} from './path_mapped_compiler_host';
import {Console} from './private_import_core';
const GENERATED_FILES = /\.ngfactory\.ts$|\.css\.ts$|\.css\.shim\.ts$/;
const GENERATED_META_FILES = /\.json$/;
const GENERATED_OR_DTS_FILES = /\.d\.ts$|\.ngfactory\.ts$|\.css\.ts$|\.css\.shim\.ts$/;
const PREAMBLE = `/**
@ -68,10 +69,11 @@ export class CodeGenerator {
sf => this.ngCompilerHost.getCanonicalFileName(sf.fileName)))
.then(generatedModules => {
generatedModules.forEach(generatedModule => {
const sourceFile = this.program.getSourceFile(generatedModule.fileUrl);
const emitPath = this.calculateEmitPath(generatedModule.moduleUrl);
this.host.writeFile(
emitPath, PREAMBLE + generatedModule.source, false, () => {}, [sourceFile]);
const sourceFile = this.program.getSourceFile(generatedModule.srcFileUrl);
const emitPath = this.calculateEmitPath(generatedModule.genFileUrl);
const source = GENERATED_META_FILES.test(emitPath) ? generatedModule.source :
PREAMBLE + generatedModule.source;
this.host.writeFile(emitPath, source, false, () => {}, [sourceFile]);
});
});
}

View File

@ -209,6 +209,12 @@ export class CompilerHost implements AotCompilerHost {
}
loadResource(filePath: string): Promise<string> { return this.context.readResource(filePath); }
loadSummary(filePath: string): string { return this.context.readFile(filePath); }
getOutputFileName(sourceFilePath: string): string {
return sourceFilePath.replace(EXT, '') + '.d.ts';
}
}
export class CompilerHostContextAdapter {

View File

@ -36,6 +36,7 @@ export * from './src/aot/compiler_host';
export * from './src/aot/static_reflector';
export * from './src/aot/static_reflection_capabilities';
export * from './src/aot/static_symbol';
export * from './src/aot/summary_resolver';
export {JitCompiler} from './src/jit/compiler';
export * from './src/jit/compiler_factory';
export * from './src/url_resolver';

View File

@ -10,7 +10,7 @@ import {SchemaMetadata} from '@angular/core';
import {AnimationCompiler} from '../animation/animation_compiler';
import {AnimationParser} from '../animation/animation_parser';
import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompilePipeMetadata, CompileProviderMetadata, createHostComponentMeta, identifierModuleUrl, identifierName} from '../compile_metadata';
import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompilePipeMetadata, CompileProviderMetadata, CompileTypeSummary, createHostComponentMeta, identifierModuleUrl, identifierName} from '../compile_metadata';
import {DirectiveNormalizer} from '../directive_normalizer';
import {DirectiveWrapperCompileResult, DirectiveWrapperCompiler} from '../directive_wrapper_compiler';
import {ListWrapper} from '../facade/collection';
@ -24,12 +24,11 @@ import {TemplateParser} from '../template_parser/template_parser';
import {ComponentFactoryDependency, DirectiveWrapperDependency, ViewClassDependency, ViewCompileResult, ViewCompiler} from '../view_compiler/view_compiler';
import {AotCompilerOptions} from './compiler_options';
import {GeneratedFile} from './generated_file';
import {StaticReflector} from './static_reflector';
import {StaticSymbol} from './static_symbol';
export class SourceModule {
constructor(public fileUrl: string, public moduleUrl: string, public source: string) {}
}
import {AotSummaryResolver} from './summary_resolver';
import {filterFileByPatterns} from './utils';
export class AotCompiler {
private _animationCompiler = new AnimationCompiler();
@ -39,13 +38,13 @@ export class AotCompiler {
private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler,
private _dirWrapperCompiler: DirectiveWrapperCompiler,
private _ngModuleCompiler: NgModuleCompiler, private _outputEmitter: OutputEmitter,
private _localeId: string, private _translationFormat: string,
private _animationParser: AnimationParser, private _staticReflector: StaticReflector,
private _options: AotCompilerOptions) {}
private _summaryResolver: AotSummaryResolver, private _localeId: string,
private _translationFormat: string, private _animationParser: AnimationParser,
private _staticReflector: StaticReflector, private _options: AotCompilerOptions) {}
clearCache() { this._metadataResolver.clearCache(); }
compileAll(rootFiles: string[]): Promise<SourceModule[]> {
compileAll(rootFiles: string[]): Promise<GeneratedFile[]> {
const programSymbols = extractProgramSymbols(this._staticReflector, rootFiles, this._options);
const {ngModuleByPipeOrDirective, files, ngModules} =
analyzeAndValidateNgModules(programSymbols, this._options, this._metadataResolver);
@ -56,18 +55,28 @@ export class AotCompiler {
.then(() => {
const sourceModules = files.map(
file => this._compileSrcFile(
file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.ngModules));
file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.pipes,
file.ngModules));
return ListWrapper.flatten(sourceModules);
});
}
private _compileSrcFile(
srcFileUrl: string, ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>,
directives: StaticSymbol[], ngModules: StaticSymbol[]): SourceModule[] {
directives: StaticSymbol[], pipes: StaticSymbol[],
ngModules: StaticSymbol[]): GeneratedFile[] {
const fileSuffix = _splitTypescriptSuffix(srcFileUrl)[1];
const statements: o.Statement[] = [];
const exportedVars: string[] = [];
const outputSourceModules: SourceModule[] = [];
const generatedFiles: GeneratedFile[] = [];
// write summary files
const summaries: CompileTypeSummary[] = [
...pipes.map(ref => this._metadataResolver.getPipeSummary(ref)),
...directives.map(ref => this._metadataResolver.getDirectiveSummary(ref)),
...ngModules.map(ref => this._metadataResolver.getNgModuleSummary(ref))
];
generatedFiles.push(this._summaryResolver.serializeSummaries(srcFileUrl, summaries));
// compile all ng modules
exportedVars.push(
@ -94,7 +103,7 @@ export class AotCompiler {
// compile styles
const stylesCompileResults = this._styleCompiler.compileComponent(compMeta);
stylesCompileResults.externalStylesheets.forEach((compiledStyleSheet) => {
outputSourceModules.push(this._codgenStyles(srcFileUrl, compiledStyleSheet, fileSuffix));
generatedFiles.push(this._codgenStyles(srcFileUrl, compiledStyleSheet, fileSuffix));
});
// compile components
@ -107,9 +116,9 @@ export class AotCompiler {
if (statements.length > 0) {
const srcModule = this._codegenSourceModule(
srcFileUrl, _ngfactoryModuleUrl(srcFileUrl), statements, exportedVars);
outputSourceModules.unshift(srcModule);
generatedFiles.unshift(srcModule);
}
return outputSourceModules;
return generatedFiles;
}
private _compileModule(ngModuleType: StaticSymbol, targetStatements: o.Statement[]): string {
@ -205,7 +214,7 @@ export class AotCompiler {
}
private _codgenStyles(
fileUrl: string, stylesCompileResult: CompiledStylesheet, fileSuffix: string): SourceModule {
fileUrl: string, stylesCompileResult: CompiledStylesheet, fileSuffix: string): GeneratedFile {
_resolveStyleStatements(this._staticReflector, stylesCompileResult, fileSuffix);
return this._codegenSourceModule(
fileUrl, _stylesModuleUrl(
@ -214,11 +223,11 @@ export class AotCompiler {
}
private _codegenSourceModule(
fileUrl: string, moduleUrl: string, statements: o.Statement[],
exportedVars: string[]): SourceModule {
return new SourceModule(
fileUrl, moduleUrl,
this._outputEmitter.emitStatements(moduleUrl, statements, exportedVars));
srcFileUrl: string, genFileUrl: string, statements: o.Statement[],
exportedVars: string[]): GeneratedFile {
return new GeneratedFile(
srcFileUrl, genFileUrl,
this._outputEmitter.emitStatements(genFileUrl, statements, exportedVars));
}
}
@ -290,7 +299,12 @@ function _splitTypescriptSuffix(path: string): string[] {
export interface NgAnalyzedModules {
ngModules: CompileNgModuleMetadata[];
ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>;
files: Array<{srcUrl: string, directives: StaticSymbol[], ngModules: StaticSymbol[]}>;
files: Array<{
srcUrl: string,
directives: StaticSymbol[],
pipes: StaticSymbol[],
ngModules: StaticSymbol[]
}>;
symbolsMissingModule?: StaticSymbol[];
}
@ -325,11 +339,13 @@ function _analyzeNgModules(
const ngModuleByPipeOrDirective = new Map<StaticSymbol, CompileNgModuleMetadata>();
const ngModulesByFile = new Map<string, StaticSymbol[]>();
const ngDirectivesByFile = new Map<string, StaticSymbol[]>();
const ngPipesByFile = new Map<string, StaticSymbol[]>();
const filePaths = new Set<string>();
// Looping over all modules to construct:
// - a map from file to modules `ngModulesByFile`,
// - a map from file to directives `ngDirectivesByFile`,
// - a map from file to pipes `ngPipesByFile`,
// - a map from directive/pipe to module `ngModuleByPipeOrDirective`.
ngModuleMetas.forEach((ngModuleMeta) => {
const srcFileUrl = ngModuleMeta.type.reference.filePath;
@ -347,16 +363,23 @@ function _analyzeNgModules(
ngModuleMeta.declaredPipes.forEach((pipeIdentifier) => {
const fileUrl = pipeIdentifier.reference.filePath;
filePaths.add(fileUrl);
ngPipesByFile.set(
fileUrl, (ngPipesByFile.get(fileUrl) || []).concat(pipeIdentifier.reference));
ngModuleByPipeOrDirective.set(pipeIdentifier.reference, ngModuleMeta);
});
});
const files: {srcUrl: string, directives: StaticSymbol[], ngModules: StaticSymbol[]}[] = [];
const files:
{srcUrl: string,
directives: StaticSymbol[],
pipes: StaticSymbol[],
ngModules: StaticSymbol[]}[] = [];
filePaths.forEach((srcUrl) => {
const directives = ngDirectivesByFile.get(srcUrl) || [];
const pipes = ngPipesByFile.get(srcUrl) || [];
const ngModules = ngModulesByFile.get(srcUrl) || [];
files.push({srcUrl, directives, ngModules});
files.push({srcUrl, directives, pipes, ngModules});
});
return {
@ -372,7 +395,7 @@ export function extractProgramSymbols(
staticReflector: StaticReflector, files: string[],
options: {includeFilePattern?: RegExp, excludeFilePattern?: RegExp} = {}): StaticSymbol[] {
const staticSymbols: StaticSymbol[] = [];
files.filter(fileName => _filterFileByPatterns(fileName, options)).forEach(sourceFile => {
files.filter(fileName => filterFileByPatterns(fileName, options)).forEach(sourceFile => {
const moduleMetadata = staticReflector.getModuleMetadata(sourceFile);
if (!moduleMetadata) {
console.error(`WARNING: no metadata found for ${sourceFile}`);
@ -410,7 +433,7 @@ function _createNgModules(
const ngModulePipesAndDirective = new Set<StaticSymbol>();
const addNgModule = (staticSymbol: any) => {
if (ngModules.has(staticSymbol) || !_filterFileByPatterns(staticSymbol.filePath, options)) {
if (ngModules.has(staticSymbol) || !filterFileByPatterns(staticSymbol.filePath, options)) {
return false;
}
const ngModule = metadataResolver.getNgModuleMetadata(staticSymbol, false);
@ -436,15 +459,3 @@ function _createNgModules(
return {ngModules: Array.from(ngModules.values()), symbolsMissingModule};
}
function _filterFileByPatterns(
fileName: string, options: {includeFilePattern?: RegExp, excludeFilePattern?: RegExp} = {}) {
let match = true;
if (options.includeFilePattern) {
match = match && !!options.includeFilePattern.exec(fileName);
}
if (options.excludeFilePattern) {
match = match && !options.excludeFilePattern.exec(fileName);
}
return match;
}

View File

@ -34,8 +34,7 @@ import {AotCompilerHost} from './compiler_host';
import {AotCompilerOptions} from './compiler_options';
import {StaticAndDynamicReflectionCapabilities} from './static_reflection_capabilities';
import {StaticReflector} from './static_reflector';
import {AotSummaryResolver} from './summary_resolver';
/**
* Creates a new AotCompiler based on options and a host.
@ -61,15 +60,17 @@ export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCom
const console = new Console();
const tmplParser =
new TemplateParser(expressionParser, elementSchemaRegistry, htmlParser, console, []);
const summaryResolver = new AotSummaryResolver(compilerHost, staticReflector, options);
const resolver = new CompileMetadataResolver(
new NgModuleResolver(staticReflector), new DirectiveResolver(staticReflector),
new PipeResolver(staticReflector), elementSchemaRegistry, normalizer, staticReflector);
new PipeResolver(staticReflector), summaryResolver, elementSchemaRegistry, normalizer,
staticReflector);
// TODO(vicb): do not pass options.i18nFormat here
const compiler = new AotCompiler(
resolver, tmplParser, new StyleCompiler(urlResolver),
new ViewCompiler(config, elementSchemaRegistry),
new DirectiveWrapperCompiler(config, expressionParser, elementSchemaRegistry, console),
new NgModuleCompiler(), new TypeScriptEmitter(compilerHost), options.locale,
new NgModuleCompiler(), new TypeScriptEmitter(compilerHost), summaryResolver, options.locale,
options.i18nFormat, new AnimationParser(elementSchemaRegistry), staticReflector, options);
return {compiler, reflector: staticReflector};
}

View File

@ -10,15 +10,16 @@ import {ImportResolver} from '../output/path_util';
import {StaticReflectorHost} from './static_reflector';
import {StaticSymbol} from './static_symbol';
import {AotSummaryResolverHost} from './summary_resolver';
/**
* The host of the AotCompiler disconnects the implementation from TypeScript / other language
* services and from underlying file systems.
*/
export interface AotCompilerHost extends StaticReflectorHost, ImportResolver {
export interface AotCompilerHost extends StaticReflectorHost, ImportResolver,
AotSummaryResolverHost {
/**
* Loads a resource (e.g. html / css)
*/
loadResource(path: string): Promise<string>;
}
}

View File

@ -0,0 +1,11 @@
/**
* @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
*/
export class GeneratedFile {
constructor(public srcFileUrl: string, public genFileUrl: string, public source: string) {}
}

View File

@ -0,0 +1,106 @@
/**
* @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 {CompileIdentifierMetadata, CompileTypeMetadata, CompileTypeSummary, identifierModuleUrl, identifierName} from '../compile_metadata';
import {SummaryResolver} from '../summary_resolver';
import {GeneratedFile} from './generated_file';
import {StaticReflector} from './static_reflector';
import {StaticSymbol, isStaticSymbol} from './static_symbol';
import {filterFileByPatterns} from './utils';
const STRIP_SRC_FILE_SUFFIXES = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
export interface AotSummaryResolverHost {
/**
* Loads an NgModule/Directive/Pipe summary file
*/
loadSummary(filePath: string): string;
/**
* Returns the output file path of a source file.
* E.g.
* `some_file.ts` -> `some_file.d.ts`
*/
getOutputFileName(sourceFilePath: string): string;
}
export interface AotSummaryResolverOptions {
includeFilePattern?: RegExp;
excludeFilePattern?: RegExp;
}
export class AotSummaryResolver implements SummaryResolver {
private summaryCache: {[srcFilePath: string]: CompileTypeSummary[]} = {};
constructor(
private host: AotSummaryResolverHost, private staticReflector: StaticReflector,
private options: AotSummaryResolverOptions) {}
serializeSummaries(srcFileUrl: string, summaries: CompileTypeSummary[]): GeneratedFile {
const jsonReplacer = (key: string, value: any) => {
if (key === 'reference' && isStaticSymbol(value)) {
// We convert the source filenames into output filenames,
// as the generated summary file will be used when the current
// compilation unit is used as a library
return {
'__symbolic__': 'symbol',
'name': value.name,
'path': this.host.getOutputFileName(value.filePath),
'members': value.members
};
}
return value;
};
return new GeneratedFile(
srcFileUrl, summaryFileName(srcFileUrl), JSON.stringify(summaries, jsonReplacer));
}
resolveSummary(staticSymbol: StaticSymbol): any {
const filePath = staticSymbol.filePath;
const name = staticSymbol.name;
if (!filterFileByPatterns(filePath, this.options)) {
let summaries = this.summaryCache[filePath];
const summaryFilePath = summaryFileName(filePath);
if (!summaries) {
try {
const jsonReviver = (key: string, value: any) => {
if (key === 'reference' && value && value['__symbolic__'] === 'symbol') {
// Note: We can't use staticReflector.findDeclaration here:
// Summary files can contain symbols of transitive compilation units
// (via the providers), and findDeclaration needs .metadata.json / .d.ts files,
// but we don't want to depend on these for transitive dependencies.
return this.staticReflector.getStaticSymbol(
value['path'], value['name'], value['members']);
} else {
return value;
}
};
summaries = JSON.parse(this.host.loadSummary(summaryFilePath), jsonReviver);
} catch (e) {
console.error(`Error loading summary file ${summaryFilePath}`);
throw e;
}
this.summaryCache[filePath] = summaries;
}
const result = summaries.find((summary) => summary.type.reference === staticSymbol);
if (!result) {
throw new Error(
`Could not find the symbol ${name} in the summary file ${summaryFilePath}!`);
}
return result;
} else {
return null;
}
}
}
function summaryFileName(fileName: string): string {
const fileNameWithoutSuffix = fileName.replace(STRIP_SRC_FILE_SUFFIXES, '');
return `${fileNameWithoutSuffix}.ngsummary.json`;
}

View File

@ -0,0 +1,19 @@
/**
* @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
*/
export function filterFileByPatterns(
fileName: string, options: {includeFilePattern?: RegExp, excludeFilePattern?: RegExp} = {}) {
let match = true;
if (options.includeFilePattern) {
match = match && !!options.includeFilePattern.exec(fileName);
}
if (options.excludeFilePattern) {
match = match && !options.excludeFilePattern.exec(fileName);
}
return match;
}

View File

@ -123,6 +123,8 @@ export interface CompileSummary {
isSummary: boolean /* TODO: `true` when we drop TS 1.8 support */;
}
export interface CompileTypeSummary extends CompileSummary { type: CompileTypeMetadata; }
export interface CompileDiDependencyMetadata {
isAttribute?: boolean;
isSelf?: boolean;
@ -262,7 +264,7 @@ export class CompileTemplateMetadata {
// Note: This should only use interfaces as nested data types
// as we need to be able to serialize this from/to JSON!
export interface CompileDirectiveSummary extends CompileSummary {
export interface CompileDirectiveSummary extends CompileTypeSummary {
isSummary: boolean /* TODO: `true` when we drop TS 1.8 support */;
type: CompileTypeMetadata;
isComponent: boolean;
@ -470,7 +472,7 @@ export function createHostComponentMeta(
});
}
export interface CompilePipeSummary extends CompileSummary {
export interface CompilePipeSummary extends CompileTypeSummary {
isSummary: boolean /* TODO: `true` when we drop TS 1.8 support */;
type: CompileTypeMetadata;
name: string;
@ -499,7 +501,7 @@ export class CompilePipeMetadata {
// Note: This should only use interfaces as nested data types
// as we need to be able to serialize this from/to JSON!
export interface CompileNgModuleSummary {
export interface CompileNgModuleSummary extends CompileTypeSummary {
isSummary: boolean /* TODO: `true` when we drop TS 1.8 support */;
type: CompileTypeMetadata;
@ -510,9 +512,9 @@ export interface CompileNgModuleSummary {
// Note: This is transitive.
entryComponents: CompileIdentifierMetadata[];
// Note: This is transitive.
providers: {provider: CompileProviderMetadata, module: CompileIdentifierMetadata}[],
// Note: This is transitive.
modules: CompileTypeMetadata[];
providers: {provider: CompileProviderMetadata, module: CompileIdentifierMetadata}[];
// Note: This is transitive.
modules: CompileTypeMetadata[];
}
/**

View File

@ -15,6 +15,7 @@ import {ViewEncapsulation} from '@angular/core';
import {analyzeAndValidateNgModules, extractProgramSymbols} from '../aot/compiler';
import {StaticAndDynamicReflectionCapabilities} from '../aot/static_reflection_capabilities';
import {StaticReflector, StaticReflectorHost} from '../aot/static_reflector';
import {AotSummaryResolver, AotSummaryResolverHost} from '../aot/summary_resolver';
import {CompileDirectiveMetadata} from '../compile_metadata';
import {CompilerConfig} from '../config';
import {DirectiveNormalizer} from '../directive_normalizer';
@ -41,7 +42,7 @@ export interface ExtractorOptions {
* The host of the Extractor disconnects the implementation from TypeScript / other language
* services and from underlying file systems.
*/
export interface ExtractorHost extends StaticReflectorHost {
export interface ExtractorHost extends StaticReflectorHost, AotSummaryResolverHost {
/**
* Loads a resource (e.g. html / css)
*/
@ -110,7 +111,8 @@ export class Extractor {
const elementSchemaRegistry = new DomElementSchemaRegistry();
const resolver = new CompileMetadataResolver(
new NgModuleResolver(staticReflector), new DirectiveResolver(staticReflector),
new PipeResolver(staticReflector), elementSchemaRegistry, normalizer, staticReflector);
new PipeResolver(staticReflector), new AotSummaryResolver(host, staticReflector, options),
elementSchemaRegistry, normalizer, staticReflector);
// TODO(vicb): implicit tags & attributes
const messageBundle = new MessageBundle(htmlParser, [], {});

View File

@ -101,18 +101,14 @@ export class JitCompiler implements Compiler {
private _loadModules(mainModule: any, isSync: boolean): Promise<any> {
const loadingPromises: Promise<any>[] = [];
const {ngModule, loading} =
this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata(mainModule, isSync);
loadingPromises.push(loading);
const ngModule = this._metadataResolver.getNgModuleMetadata(mainModule);
// Note: the loadingPromise for a module only includes the loading of the exported directives
// of imported modules.
// However, for runtime compilation, we want to transitively compile all modules,
// so we also need to call loadNgModuleMetadata for all nested modules.
ngModule.transitiveModule.modules.forEach((localModuleMeta) => {
loadingPromises.push(
this._metadataResolver
.loadNgModuleDirectiveAndPipeMetadata(localModuleMeta.reference, isSync)
.loading);
loadingPromises.push(this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
localModuleMeta.reference, isSync));
});
return Promise.all(loadingPromises);
}

View File

@ -26,6 +26,7 @@ import {ResourceLoader} from '../resource_loader';
import {DomElementSchemaRegistry} from '../schema/dom_element_schema_registry';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {StyleCompiler} from '../style_compiler';
import {SummaryResolver} from '../summary_resolver';
import {TemplateParser} from '../template_parser/template_parser';
import {DEFAULT_PACKAGE_URL_PROVIDER, UrlResolver} from '../url_resolver';
import {ViewCompiler} from '../view_compiler/view_compiler';
@ -46,6 +47,7 @@ export const COMPILER_PROVIDERS: Array<any|Type<any>|{[k: string]: any}|any[]> =
{provide: Reflector, useValue: reflector},
{provide: ReflectorReader, useExisting: Reflector},
{provide: ResourceLoader, useValue: _NO_RESOURCE_LOADER},
SummaryResolver,
Console,
Lexer,
Parser,

View File

@ -21,6 +21,7 @@ import {NgModuleResolver} from './ng_module_resolver';
import {PipeResolver} from './pipe_resolver';
import {ComponentStillLoadingError, LIFECYCLE_HOOKS_VALUES, ReflectorReader, reflector} from './private_import_core';
import {ElementSchemaRegistry} from './schema/element_schema_registry';
import {SummaryResolver} from './summary_resolver';
import {getUrlScheme} from './url_resolver';
import {MODULE_SUFFIX, SyncAsyncResult, ValueTransformer, visitValue} from './util';
@ -39,12 +40,14 @@ export class CompileMetadataResolver {
private _directiveSummaryCache = new Map<Type<any>, cpl.CompileDirectiveSummary>();
private _pipeCache = new Map<Type<any>, cpl.CompilePipeMetadata>();
private _pipeSummaryCache = new Map<Type<any>, cpl.CompilePipeSummary>();
private _ngModuleSummaryCache = new Map<Type<any>, cpl.CompileNgModuleSummary>();
private _ngModuleCache = new Map<Type<any>, cpl.CompileNgModuleMetadata>();
private _ngModuleOfTypes = new Map<Type<any>, Type<any>>();
constructor(
private _ngModuleResolver: NgModuleResolver, private _directiveResolver: DirectiveResolver,
private _pipeResolver: PipeResolver, private _schemaRegistry: ElementSchemaRegistry,
private _pipeResolver: PipeResolver, private _summaryResolver: SummaryResolver,
private _schemaRegistry: ElementSchemaRegistry,
private _directiveNormalizer: DirectiveNormalizer,
private _reflector: ReflectorReader = reflector) {}
@ -54,6 +57,7 @@ export class CompileMetadataResolver {
this._directiveSummaryCache.delete(type);
this._pipeCache.delete(type);
this._pipeSummaryCache.delete(type);
this._ngModuleSummaryCache.delete(type);
this._ngModuleOfTypes.delete(type);
// Clear all of the NgModule as they contain transitive information!
this._ngModuleCache.clear();
@ -69,6 +73,7 @@ export class CompileMetadataResolver {
this._pipeSummaryCache.clear();
this._ngModuleCache.clear();
this._ngModuleOfTypes.clear();
this._ngModuleSummaryCache.clear();
this._directiveNormalizer.clearCache();
}
@ -126,7 +131,7 @@ export class CompileMetadataResolver {
return null;
}
private _loadDirectiveMetadata(directiveType: any, isSync: boolean): () => Promise<any> {
private _loadDirectiveMetadata(directiveType: any, isSync: boolean): Promise<any> {
if (this._directiveCache.has(directiveType)) {
return;
}
@ -176,7 +181,7 @@ export class CompileMetadataResolver {
if (isSync) {
throw new ComponentStillLoadingError(directiveType);
}
return () => templateMeta.asyncResult.then(createDirectiveMetadata);
return templateMeta.asyncResult.then(createDirectiveMetadata);
}
} else {
// directive
@ -288,10 +293,15 @@ export class CompileMetadataResolver {
}
getDirectiveSummary(dirType: any): cpl.CompileDirectiveSummary {
const dirSummary = this._directiveSummaryCache.get(dirType);
let dirSummary = this._directiveSummaryCache.get(dirType);
if (!dirSummary) {
throw new Error(
`Illegal state: getDirectiveSummary can only be called after loadNgModuleMetadata for a module that imports it. Directive ${stringify(dirType)}.`);
dirSummary = <cpl.CompileDirectiveSummary>this._summaryResolver.resolveSummary(dirType);
if (dirSummary) {
this._directiveSummaryCache.set(dirType, dirSummary);
} else {
throw new Error(
`Illegal state: Could not load the summary for directive ${stringify(dirType)}.`);
}
}
return dirSummary;
}
@ -300,33 +310,38 @@ export class CompileMetadataResolver {
isPipe(type: any) { return this._pipeResolver.isPipe(type); }
private _getNgModuleSummary(moduleType: any): cpl.CompileNgModuleSummary {
// TODO(tbosch): add logic to read summary files!
// - needs to add directive / pipe summaries to this._directiveSummaryCache /
// this._pipeSummaryCache as well!
const moduleMeta = this.getNgModuleMetadata(moduleType, false);
return moduleMeta ? moduleMeta.toSummary() : null;
getNgModuleSummary(moduleType: any): cpl.CompileNgModuleSummary {
let moduleSummary = this._ngModuleSummaryCache.get(moduleType);
if (!moduleSummary) {
moduleSummary = <cpl.CompileNgModuleSummary>this._summaryResolver.resolveSummary(moduleType);
if (!moduleSummary) {
const moduleMeta = this.getNgModuleMetadata(moduleType, false);
moduleSummary = moduleMeta ? moduleMeta.toSummary() : null;
}
if (moduleSummary) {
this._ngModuleSummaryCache.set(moduleType, moduleSummary);
}
}
return moduleSummary;
}
/**
* Loads an NgModule and all of its directives. This includes loading the exported directives of
* imported modules,
* but not private directives of imported modules.
* Loads the declared directives and pipes of an NgModule.
*/
loadNgModuleDirectiveAndPipeMetadata(moduleType: any, isSync: boolean, throwIfNotFound = true):
{ngModule: cpl.CompileNgModuleMetadata, loading: Promise<any>} {
Promise<any> {
const ngModule = this.getNgModuleMetadata(moduleType, throwIfNotFound);
let loading: Promise<any>;
const loading: Promise<any>[] = [];
if (ngModule) {
loading = Promise.all([
...ngModule.transitiveModule.directives.map(
(id) => this._loadDirectiveMetadata(id.reference, isSync)),
...ngModule.transitiveModule.pipes.map((id) => this._loadPipeMetadata(id.reference)),
]);
} else {
loading = Promise.resolve(null);
ngModule.declaredDirectives.forEach((id) => {
const promise = this._loadDirectiveMetadata(id.reference, isSync);
if (promise) {
loading.push(promise);
}
});
ngModule.declaredPipes.forEach((id) => this._loadPipeMetadata(id.reference));
}
return {ngModule, loading};
return Promise.all(loading);
}
getNgModuleMetadata(moduleType: any, throwIfNotFound = true): cpl.CompileNgModuleMetadata {
@ -365,7 +380,7 @@ export class CompileMetadataResolver {
}
if (importedModuleType) {
const importedModuleSummary = this._getNgModuleSummary(importedModuleType);
const importedModuleSummary = this.getNgModuleSummary(importedModuleType);
if (!importedModuleSummary) {
throw new Error(
`Unexpected ${this._getTypeDescriptor(importedType)} '${stringify(importedType)}' imported by the module '${stringify(moduleType)}'`);
@ -384,7 +399,7 @@ export class CompileMetadataResolver {
throw new Error(
`Unexpected value '${stringify(exportedType)}' exported by the module '${stringify(moduleType)}'`);
}
const exportedModuleSummary = this._getNgModuleSummary(exportedType);
const exportedModuleSummary = this.getNgModuleSummary(exportedType);
if (exportedModuleSummary) {
exportedModules.push(exportedModuleSummary);
} else {
@ -543,6 +558,7 @@ export class CompileMetadataResolver {
entryComponents.push(comp);
}
});
const addedTokens = new Set<any>();
modSummary.providers.forEach((entry) => {
const tokenRef = cpl.tokenReference(entry.provider.token);
let prevModules = modulesByToken.get(tokenRef);
@ -551,8 +567,11 @@ export class CompileMetadataResolver {
modulesByToken.set(tokenRef, prevModules);
}
const moduleRef = entry.module.reference;
if (!prevModules.has(moduleRef)) {
// Note: the providers of one module may still contain multiple providers
// per token (e.g. for multi providers), and we need to preserve these.
if (addedTokens.has(tokenRef) || !prevModules.has(moduleRef)) {
prevModules.add(moduleRef);
addedTokens.add(tokenRef);
providers.push(entry);
}
});
@ -575,7 +594,7 @@ export class CompileMetadataResolver {
visitedModules.add(ngModule.type.reference);
targetModules.push(ngModule);
this._getTransitiveExportedModules(
ngModule.exportedModules.map(id => this._getNgModuleSummary(id.reference)),
ngModule.exportedModules.map(id => this.getNgModuleSummary(id.reference)),
targetModules, visitedModules);
}
});
@ -617,10 +636,15 @@ export class CompileMetadataResolver {
}
getPipeSummary(pipeType: any): cpl.CompilePipeSummary {
const pipeSummary = this._pipeSummaryCache.get(pipeType);
let pipeSummary = this._pipeSummaryCache.get(pipeType);
if (!pipeSummary) {
throw new Error(
`Illegal state: getPipeSummary can only be called after loadNgModuleMetadata for a module that imports it. Pipe ${stringify(pipeType)}.`);
pipeSummary = <cpl.CompilePipeSummary>this._summaryResolver.resolveSummary(pipeType);
if (pipeSummary) {
this._pipeSummaryCache.set(pipeType, pipeSummary);
} else {
throw new Error(
`Illegal state: Could not load the summary for pipe ${stringify(pipeType)}.`);
}
}
return pipeSummary;
}

View File

@ -0,0 +1,14 @@
/**
* @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 {Injectable} from '@angular/core';
import {CompileSummary} from './compile_metadata';
@Injectable()
export class SummaryResolver {
resolveSummary(reference: any): CompileSummary { return null; }
}

View File

@ -694,7 +694,7 @@ describe('StaticReflector', () => {
});
class MockStaticReflectorHost implements StaticReflectorHost {
export class MockStaticReflectorHost implements StaticReflectorHost {
private collector = new MetadataCollector();
constructor(private data: {[key: string]: any}) {}

View File

@ -0,0 +1,93 @@
/**
* @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 {AotSummaryResolver, AotSummaryResolverHost, CompileTypeSummary, StaticReflector, StaticReflectorHost, StaticSymbol} from '@angular/compiler';
import * as path from 'path';
import {MockStaticReflectorHost} from './static_reflector_spec';
const EXT = /\.ts$|.d.ts$/;
export function main() {
describe('AotSummaryResolver', () => {
let resolver: AotSummaryResolver;
let staticReflector: StaticReflector;
function init(summaries: {[filePath: string]: string} = {}) {
// Note: We don't give the static reflector metadata files,
// so that we can test that we can deserialize summary files
// without reading metadata files. This is important
// as summary files can contain references to files of transitive compilation
// dependencies, and we don't want to read their metadata files.
staticReflector = new StaticReflector(new MockStaticReflectorHost({}));
const host = new MockAotSummaryResolverHost(summaries);
resolver = new AotSummaryResolver(host, staticReflector, {excludeFilePattern: /\.d\.ts$/});
}
it('should add .ngsummary.json to the filename', () => {
init();
expect(resolver.serializeSummaries('a.ts', []).genFileUrl).toBe('a.ngsummary.json');
expect(resolver.serializeSummaries('a.d.ts', []).genFileUrl).toBe('a.ngsummary.json');
expect(resolver.serializeSummaries('a.js', []).genFileUrl).toBe('a.ngsummary.json');
});
it('should serialize plain data', () => {
init();
const data = <any>[{a: 'b'}];
expect(JSON.parse(resolver.serializeSummaries('someSourceFile', data).source)).toEqual(data);
});
it('should serialize summary for .ts files and deserialize based on .d.ts files', () => {
init();
const serializedData = resolver.serializeSummaries(
'/tmp/some_class.ts', [{
isSummary: true,
type: {
reference: staticReflector.getStaticSymbol('/tmp/some_class.ts', 'SomeClass'),
diDeps: [],
lifecycleHooks: []
}
}]);
// Note: this creates a new staticReflector!
init({[serializedData.genFileUrl]: serializedData.source});
expect(resolver.resolveSummary(
staticReflector.getStaticSymbol('/tmp/some_class.d.ts', 'SomeClass')))
.toEqual({
isSummary: true,
type: {
reference: staticReflector.getStaticSymbol('/tmp/some_class.d.ts', 'SomeClass'),
diDeps: [],
lifecycleHooks: []
}
});
});
});
}
class MockAotSummaryResolverHost implements AotSummaryResolverHost {
constructor(private summaries: {[fileName: string]: string}) {}
loadSummary(filePath: string): string {
const result = this.summaries[filePath];
if (!result) {
throw new Error(`Could not find summary for ${filePath}`);
}
return result;
}
fileNameToModuleName(fileName: string): string {
return './' + path.basename(fileName).replace(EXT, '');
}
getOutputFileName(sourceFileName: string): string {
return sourceFileName.replace(EXT, '') + '.d.ts';
}
}

View File

@ -84,7 +84,7 @@ export function main() {
}
resourceLoader.when('someTemplateUrl', 'someTemplate');
resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, false).loading.then(() => {
resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, false).then(() => {
const meta = resolver.getDirectiveMetadata(ComponentWithExternalResources);
expect(meta.selector).toEqual('someSelector');
expect(meta.template.styleUrls).toEqual(['someStyleUrl']);
@ -94,29 +94,6 @@ export function main() {
resourceLoader.flush();
})));
it('should wait for external resources of imported modules',
async(inject(
[CompileMetadataResolver, ResourceLoader],
(resolver: CompileMetadataResolver, resourceLoader: MockResourceLoader) => {
@NgModule({
declarations: [ComponentWithExternalResources],
exports: [ComponentWithExternalResources]
})
class SomeImportedModule {
}
@NgModule({imports: [SomeImportedModule]})
class SomeModule {
}
resourceLoader.when('someTemplateUrl', 'someTemplate');
resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, false).loading.then(() => {
const meta = resolver.getDirectiveMetadata(ComponentWithExternalResources);
expect(meta.selector).toEqual('someSelector');
});
resourceLoader.flush();
})));
it('should use `./` as base url for templates during runtime compilation if no moduleId is given',
async(inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@Component({selector: 'someComponent', templateUrl: 'someUrl'})
@ -128,7 +105,7 @@ export function main() {
class SomeModule {
}
resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, false).loading.then(() => {
resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, false).then(() => {
const value: string =
resolver.getDirectiveMetadata(ComponentWithoutModuleId).template.templateUrl;
const expectedEndValue = './someUrl';
@ -329,7 +306,7 @@ export function main() {
class MyModule {
}
const modMeta = resolver.loadNgModuleDirectiveAndPipeMetadata(MyModule, true).ngModule;
const modMeta = resolver.getNgModuleMetadata(MyModule);
expect(modMeta.declaredDirectives.length).toBe(1);
expect(modMeta.declaredDirectives[0].reference).toBe(MyComp);
}));

View File

@ -18,6 +18,7 @@ import {NgModuleResolver} from '@angular/compiler/src/ng_module_resolver';
import {PipeResolver} from '@angular/compiler/src/pipe_resolver';
import {ResourceLoader} from '@angular/compiler/src/resource_loader';
import {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry';
import {SummaryResolver} from '@angular/compiler/src/summary_resolver';
import {UrlResolver} from '@angular/compiler/src/url_resolver';
import {Type, ViewEncapsulation} from '@angular/core';
import * as fs from 'fs';
@ -124,8 +125,8 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
new DirectiveNormalizer(resourceLoader, urlResolver, htmlParser, config);
result = this._resolver = new CompileMetadataResolver(
moduleResolver, directiveResolver, pipeResolver, elementSchemaRegistry,
directiveNormalizer, this.reflector);
moduleResolver, directiveResolver, pipeResolver, new SummaryResolver(),
elementSchemaRegistry, directiveNormalizer, this.reflector);
}
return result;
}

View File

@ -50,6 +50,8 @@ cp -v package.json $TMP
./node_modules/.bin/ng-xi18n -p tsconfig-build.json --i18nFormat=xlf
./node_modules/.bin/ng-xi18n -p tsconfig-build.json --i18nFormat=xmb
node test/test_summaries.js
./node_modules/.bin/jasmine init
# Run compiler-cli integration tests in node
./node_modules/.bin/webpack ./webpack.config.js