diff --git a/packages/compiler-cli/integrationtest/src/jit_summaries.html b/packages/compiler-cli/integrationtest/src/jit_summaries.html new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/packages/compiler-cli/integrationtest/src/jit_summaries.html @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/packages/compiler-cli/integrationtest/src/jit_summaries.ts b/packages/compiler-cli/integrationtest/src/jit_summaries.ts new file mode 100644 index 0000000000..8d2bccf698 --- /dev/null +++ b/packages/compiler-cli/integrationtest/src/jit_summaries.ts @@ -0,0 +1,52 @@ +/** + * @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 {Component, Directive, Injectable, NgModule, Pipe} from '@angular/core'; + +const instances = new Map(); + +export function expectInstanceCreated(type: any) { + const instance = instances.get(type) !; + expect(instance).toBeDefined(); + expect(instance.dep instanceof SomeDep).toBe(true); +} + +export class SomeDep {} + +export class Base { + constructor(public dep: SomeDep) { instances.set(Object.getPrototypeOf(this).constructor, this); } +} + +@Component({templateUrl: './jit_summaries.html'}) +export class SomePrivateComponent extends Base { +} + +@Component({templateUrl: './jit_summaries.html'}) +export class SomePublicComponent extends Base { +} + +@Directive({selector: '[someDir]'}) +export class SomeDirective extends Base { +} + +@Pipe({name: 'somePipe'}) +export class SomePipe extends Base { + transform(value: any) { return value; } +} + +@Injectable() +export class SomeService extends Base { +} + +@NgModule({ + declarations: [SomePublicComponent, SomePrivateComponent, SomeDirective, SomePipe], + exports: [SomeDirective, SomePipe, SomePublicComponent], + providers: [SomeService] +}) +export class SomeModule extends Base { +} \ No newline at end of file diff --git a/packages/compiler-cli/integrationtest/src/module.ts b/packages/compiler-cli/integrationtest/src/module.ts index e6290e211c..ed3bcec7ed 100644 --- a/packages/compiler-cli/integrationtest/src/module.ts +++ b/packages/compiler-cli/integrationtest/src/module.ts @@ -28,6 +28,9 @@ import {CompUsingRootModuleDirectiveAndPipe, SomeDirectiveInRootModule, SomeLibM import {CompWithNgContent, ProjectingComp} from './projection'; import {CompForChildQuery, CompWithChildQuery, CompWithDirectiveChild, DirectiveForQuery} from './queries'; +// Adding an export here so that TypeScript compiles the file as well +export {SomeModule as JitSummariesSomeModule} from './jit_summaries'; + @NgModule({ declarations: [ AnimateCmp, diff --git a/packages/compiler-cli/integrationtest/test/all_spec.ts b/packages/compiler-cli/integrationtest/test/all_spec.ts index 2bf078ec81..a44aee27b9 100644 --- a/packages/compiler-cli/integrationtest/test/all_spec.ts +++ b/packages/compiler-cli/integrationtest/test/all_spec.ts @@ -16,4 +16,5 @@ import './i18n_spec'; import './ng_module_spec'; import './projection_spec'; import './query_spec'; -import './source_map_spec'; \ No newline at end of file +import './source_map_spec'; +import './jit_summaries_spec'; diff --git a/packages/compiler-cli/integrationtest/test/jit_summaries_spec.ts b/packages/compiler-cli/integrationtest/test/jit_summaries_spec.ts new file mode 100644 index 0000000000..2a6547c409 --- /dev/null +++ b/packages/compiler-cli/integrationtest/test/jit_summaries_spec.ts @@ -0,0 +1,73 @@ +/** + * @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 {Component} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {ServerTestingModule, platformServerTesting} from '@angular/platform-server/testing'; + +import {SomeDep, SomeDirective, SomeModule, SomePipe, SomePrivateComponent, SomeService, expectInstanceCreated} from '../src/jit_summaries'; +import {SomeModuleNgSummary} from '../src/jit_summaries.ngsummary'; + +describe('Jit Summaries', () => { + beforeEach(() => { + TestBed.initTestEnvironment(ServerTestingModule, platformServerTesting(), SomeModuleNgSummary); + }); + + afterEach(() => { TestBed.resetTestEnvironment(); }); + + it('should use directive metadata from summaries', () => { + @Component({template: '
'}) + class TestComp { + } + + TestBed.configureTestingModule({providers: [SomeDep], declarations: [TestComp, SomeDirective]}) + .createComponent(TestComp); + expectInstanceCreated(SomeDirective); + }); + + it('should use pipe metadata from summaries', () => { + @Component({template: '{{1 | somePipe}}'}) + class TestComp { + } + + TestBed.configureTestingModule({providers: [SomeDep], declarations: [TestComp, SomePipe]}) + .createComponent(TestComp); + expectInstanceCreated(SomePipe); + }); + + it('should use Service metadata from summaries', () => { + TestBed.configureTestingModule({ + providers: [SomeService, SomeDep], + }); + TestBed.get(SomeService); + expectInstanceCreated(SomeService); + }); + + it('should use NgModule metadata from summaries', () => { + @Component({template: '
{{1 | somePipe}}
'}) + class TestComp { + constructor(service: SomeService) {} + } + + TestBed + .configureTestingModule( + {providers: [SomeDep], declarations: [TestComp], imports: [SomeModule]}) + .createComponent(TestComp); + + expectInstanceCreated(SomeModule); + expectInstanceCreated(SomeDirective); + expectInstanceCreated(SomePipe); + expectInstanceCreated(SomeService); + }); + + it('should allow to create private components from imported NgModule summaries', () => { + TestBed.configureTestingModule({providers: [SomeDep], imports: [SomeModule]}) + .createComponent(SomePrivateComponent); + expectInstanceCreated(SomePrivateComponent); + }); +}); \ No newline at end of file diff --git a/packages/compiler-cli/integrationtest/test/util.ts b/packages/compiler-cli/integrationtest/test/util.ts index 3388ec1064..0fe1ae64d1 100644 --- a/packages/compiler-cli/integrationtest/test/util.ts +++ b/packages/compiler-cli/integrationtest/test/util.ts @@ -8,14 +8,14 @@ import {NgModuleRef} from '@angular/core'; import {ComponentFixture} from '@angular/core/testing'; -import {platformServer} from '@angular/platform-server'; +import {platformServerTesting} from '@angular/platform-server/testing'; import {MainModule} from '../src/module'; import {MainModuleNgFactory} from '../src/module.ngfactory'; let mainModuleRef: NgModuleRef = null !; beforeEach((done) => { - platformServer().bootstrapModuleFactory(MainModuleNgFactory).then((moduleRef: any) => { + platformServerTesting().bootstrapModuleFactory(MainModuleNgFactory).then((moduleRef: any) => { mainModuleRef = moduleRef; done(); }); diff --git a/packages/compiler-cli/src/compiler_host.ts b/packages/compiler-cli/src/compiler_host.ts index 79f6ae535a..bc14bbae02 100644 --- a/packages/compiler-cli/src/compiler_host.ts +++ b/packages/compiler-cli/src/compiler_host.ts @@ -15,9 +15,9 @@ import * as ts from 'typescript'; const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/; const DTS = /\.d\.ts$/; const NODE_MODULES = '/node_modules/'; -const IS_GENERATED = /\.(ngfactory|ngstyle)$/; -const GENERATED_FILES = /\.ngfactory\.ts$|\.ngstyle\.ts$/; -const GENERATED_OR_DTS_FILES = /\.d\.ts$|\.ngfactory\.ts$|\.ngstyle\.ts$/; +const IS_GENERATED = /\.(ngfactory|ngstyle|ngsummary)$/; +const GENERATED_FILES = /\.ngfactory\.ts$|\.ngstyle\.ts$|\.ngsummary\.ts$/; +const GENERATED_OR_DTS_FILES = /\.d\.ts$|\.ngfactory\.ts$|\.ngstyle\.ts$|\.ngsummary\.ts$/; const SHALLOW_IMPORT = /^((\w|-)+|(@(\w|-)+(\/(\w|-)+)+))$/; export interface CompilerHostContext extends ts.ModuleResolutionHost { diff --git a/packages/compiler/src/aot/compiler.ts b/packages/compiler/src/aot/compiler.ts index 2fd7aac5f8..aaccce05af 100644 --- a/packages/compiler/src/aot/compiler.ts +++ b/packages/compiler/src/aot/compiler.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileProviderMetadata, CompileTypeSummary, componentFactoryName, createHostComponentMeta, flatten, identifierName, sourceUrl, templateSourceUrl} from '../compile_metadata'; +import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileNgModuleSummary, CompilePipeMetadata, CompileProviderMetadata, CompileSummaryKind, CompileTypeMetadata, CompileTypeSummary, componentFactoryName, createHostComponentMeta, flatten, identifierName, sourceUrl, templateSourceUrl} from '../compile_metadata'; import {CompilerConfig} from '../config'; import {Identifiers, createIdentifier, createIdentifierToken} from '../identifiers'; import {CompileMetadataResolver} from '../metadata_resolver'; @@ -22,9 +22,9 @@ import {ViewCompiler} from '../view_compiler/view_compiler'; import {AotCompilerHost} from './compiler_host'; import {GeneratedFile} from './generated_file'; import {StaticSymbol} from './static_symbol'; -import {StaticSymbolResolver} from './static_symbol_resolver'; +import {ResolvedStaticSymbol, StaticSymbolResolver} from './static_symbol_resolver'; import {serializeSummaries} from './summary_serializer'; -import {ngfactoryFilePath, splitTypescriptSuffix, summaryFileName} from './util'; +import {ngfactoryFilePath, splitTypescriptSuffix, summaryFileName, summaryForJitFileName, summaryForJitName} from './util'; export class AotCompiler { constructor( @@ -59,12 +59,12 @@ export class AotCompiler { srcFileUrl: string, ngModuleByPipeOrDirective: Map, directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: StaticSymbol[], injectables: StaticSymbol[]): GeneratedFile[] { - const fileSuffix = splitTypescriptSuffix(srcFileUrl)[1]; + const fileSuffix = splitTypescriptSuffix(srcFileUrl, true)[1]; const statements: o.Statement[] = []; const exportedVars: string[] = []; const generatedFiles: GeneratedFile[] = []; - generatedFiles.push(this._createSummary( + generatedFiles.push(...this._createSummary( srcFileUrl, directives, pipes, ngModules, injectables, statements, exportedVars)); // compile all ng modules @@ -101,7 +101,7 @@ export class AotCompiler { }); if (statements.length > 0) { const srcModule = this._codegenSourceModule( - srcFileUrl, ngfactoryFilePath(srcFileUrl), statements, exportedVars); + srcFileUrl, ngfactoryFilePath(srcFileUrl, true), statements, exportedVars); generatedFiles.unshift(srcModule); } return generatedFiles; @@ -110,23 +110,45 @@ export class AotCompiler { private _createSummary( srcFileUrl: string, directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: StaticSymbol[], injectables: StaticSymbol[], targetStatements: o.Statement[], - targetExportedVars: string[]): GeneratedFile { + targetExportedVars: string[]): GeneratedFile[] { const symbolSummaries = this._symbolResolver.getSymbolsOf(srcFileUrl) .map(symbol => this._symbolResolver.resolveSymbol(symbol)); - const typeSummaries: CompileTypeSummary[] = [ - ...ngModules.map(ref => this._metadataResolver.getNgModuleSummary(ref) !), - ...directives.map(ref => this._metadataResolver.getDirectiveSummary(ref) !), - ...pipes.map(ref => this._metadataResolver.getPipeSummary(ref) !), - ...injectables.map(ref => this._metadataResolver.getInjectableSummary(ref) !) - ]; - const {json, exportAs} = serializeSummaries( - this._summaryResolver, this._symbolResolver, symbolSummaries, typeSummaries); + const typeData: { + summary: CompileTypeSummary, + metadata: CompileNgModuleMetadata | CompileDirectiveMetadata | CompilePipeMetadata | + CompileTypeMetadata + }[] = + [ + ...ngModules.map(ref => ({ + summary: this._metadataResolver.getNgModuleSummary(ref) !, + metadata: this._metadataResolver.getNgModuleMetadata(ref) ! + })), + ...directives.map(ref => ({ + summary: this._metadataResolver.getDirectiveSummary(ref) !, + metadata: this._metadataResolver.getDirectiveMetadata(ref) ! + })), + ...pipes.map(ref => ({ + summary: this._metadataResolver.getPipeSummary(ref) !, + metadata: this._metadataResolver.getPipeMetadata(ref) ! + })), + ...injectables.map(ref => ({ + summary: this._metadataResolver.getInjectableSummary(ref) !, + metadata: this._metadataResolver.getInjectableSummary(ref) !.type + })) + ]; + const {json, exportAs, forJit} = + serializeSummaries(this._summaryResolver, this._symbolResolver, symbolSummaries, typeData); exportAs.forEach((entry) => { targetStatements.push( o.variable(entry.exportAs).set(o.importExpr({reference: entry.symbol})).toDeclStmt()); targetExportedVars.push(entry.exportAs); }); - return new GeneratedFile(srcFileUrl, summaryFileName(srcFileUrl), json); + return [ + new GeneratedFile(srcFileUrl, summaryFileName(srcFileUrl), json), + this._codegenSourceModule( + srcFileUrl, summaryForJitFileName(srcFileUrl, true), forJit.statements, + forJit.exportedVars) + ]; } private _compileModule(ngModuleType: StaticSymbol, targetStatements: o.Statement[]): string { diff --git a/packages/compiler/src/aot/compiler_factory.ts b/packages/compiler/src/aot/compiler_factory.ts index 397aae195c..3a8b6ad7f2 100644 --- a/packages/compiler/src/aot/compiler_factory.ts +++ b/packages/compiler/src/aot/compiler_factory.ts @@ -35,7 +35,6 @@ import {StaticSymbolResolver} from './static_symbol_resolver'; import {AotSummaryResolver} from './summary_resolver'; - /** * Creates a new AotCompiler based on options and a host. */ @@ -69,16 +68,10 @@ export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCom new PipeResolver(staticReflector), summaryResolver, elementSchemaRegistry, normalizer, console, symbolCache, staticReflector); // TODO(vicb): do not pass options.i18nFormat here - const importResolver = { - getImportAs: (symbol: StaticSymbol) => symbolResolver.getImportAs(symbol) !, - fileNameToModuleName: (fileName: string, containingFilePath: string) => - compilerHost.fileNameToModuleName(fileName, containingFilePath), - getTypeArity: (symbol: StaticSymbol) => symbolResolver.getTypeArity(symbol) ! - }; const viewCompiler = new ViewCompiler(config, elementSchemaRegistry); const compiler = new AotCompiler( config, compilerHost, resolver, tmplParser, new StyleCompiler(urlResolver), viewCompiler, - new NgModuleCompiler(), new TypeScriptEmitter(importResolver), summaryResolver, + new NgModuleCompiler(), new TypeScriptEmitter(symbolResolver), summaryResolver, options.locale || null, options.i18nFormat || null, options.genFilePreamble || null, symbolResolver); return {compiler, reflector: staticReflector}; diff --git a/packages/compiler/src/aot/compiler_host.ts b/packages/compiler/src/aot/compiler_host.ts index f7a83970d8..ec6c1ff527 100644 --- a/packages/compiler/src/aot/compiler_host.ts +++ b/packages/compiler/src/aot/compiler_host.ts @@ -14,14 +14,6 @@ import {AotSummaryResolverHost} from './summary_resolver'; * services and from underlying file systems. */ export interface AotCompilerHost extends StaticSymbolResolverHost, AotSummaryResolverHost { - /** - * 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`. - * - * See ImportResolver. - */ - fileNameToModuleName(importedFilePath: string, containingFilePath: string): string|null; - /** * Loads a resource (e.g. html / css) */ diff --git a/packages/compiler/src/aot/static_symbol_resolver.ts b/packages/compiler/src/aot/static_symbol_resolver.ts index 8e0f645869..6577119247 100644 --- a/packages/compiler/src/aot/static_symbol_resolver.ts +++ b/packages/compiler/src/aot/static_symbol_resolver.ts @@ -10,7 +10,7 @@ import {SummaryResolver} from '../summary_resolver'; import {ValueTransformer, visitValue} from '../util'; import {StaticSymbol, StaticSymbolCache} from './static_symbol'; -import {isNgFactoryFile} from './util'; +import {isGeneratedFile, stripSummaryForJitFileSuffix, stripSummaryForJitNameSuffix, summaryForJitFileName, summaryForJitName} from './util'; export class ResolvedStaticSymbol { constructor(public symbol: StaticSymbol, public metadata: any) {} @@ -39,6 +39,13 @@ export interface StaticSymbolResolverHost { * `path/to/containingFile.ts` containing `import {...} from 'module-name'`. */ moduleNameToFileName(moduleName: string, containingFile?: string): string|null; + /** + * 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`. + * + * See ImportResolver. + */ + fileNameToModuleName(importedFilePath: string, containingFilePath: string): string|null; } const SUPPORTED_SCHEMA_VERSION = 3; @@ -60,6 +67,7 @@ export class StaticSymbolResolver { private importAs = new Map(); private symbolResourcePaths = new Map(); private symbolFromFile = new Map(); + private knownFileNameToModuleNames = new Map(); constructor( private host: StaticSymbolResolverHost, private staticSymbolCache: StaticSymbolCache, @@ -103,6 +111,18 @@ export class StaticSymbolResolver { this.getStaticSymbol(baseImportAs.filePath, baseImportAs.name, staticSymbol.members) : null; } + const summarizedFileName = stripSummaryForJitFileSuffix(staticSymbol.filePath); + if (summarizedFileName !== staticSymbol.filePath) { + const summarizedName = stripSummaryForJitNameSuffix(staticSymbol.name); + const baseSymbol = + this.getStaticSymbol(summarizedFileName, summarizedName, staticSymbol.members); + const baseImportAs = this.getImportAs(baseSymbol); + return baseImportAs ? + this.getStaticSymbol( + summaryForJitFileName(baseImportAs.filePath), summaryForJitName(baseImportAs.name), + baseSymbol.members) : + null; + } let result = this.summaryResolver.getImportAs(staticSymbol); if (!result) { result = this.importAs.get(staticSymbol) !; @@ -124,11 +144,11 @@ export class StaticSymbolResolver { * has. If the symbol is not a type the result is null. */ getTypeArity(staticSymbol: StaticSymbol): number|null { - // If the file is a factory file, don't resolve the symbol as doing so would - // cause the metadata for an factory file to be loaded which doesn't exist. + // If the file is a factory/ngsummary file, don't resolve the symbol as doing so would + // cause the metadata for an factory/ngsummary file to be loaded which doesn't exist. // All references to generated classes must include the correct arity whenever // generating code. - if (isNgFactoryFile(staticSymbol.filePath)) { + if (isGeneratedFile(staticSymbol.filePath)) { return null; } let resolvedSymbol = this.resolveSymbol(staticSymbol); @@ -138,6 +158,17 @@ export class StaticSymbolResolver { return (resolvedSymbol && resolvedSymbol.metadata && resolvedSymbol.metadata.arity) || null; } + /** + * Converts a file path to a module name that can be used as an `import`. + */ + fileNameToModuleName(importedFilePath: string, containingFilePath: string): string|null { + if (importedFilePath === containingFilePath) { + return null; + } + return this.knownFileNameToModuleNames.get(importedFilePath) || + this.host.fileNameToModuleName(importedFilePath, containingFilePath); + } + recordImportAs(sourceSymbol: StaticSymbol, targetSymbol: StaticSymbol) { sourceSymbol.assertNoMembers(); targetSymbol.assertNoMembers(); @@ -226,6 +257,11 @@ export class StaticSymbolResolver { this.resolvedFilePaths.add(filePath); const resolvedSymbols: ResolvedStaticSymbol[] = []; const metadata = this.getModuleMetadata(filePath); + if (metadata['importAs']) { + // Index bundle indices should use the importAs module name defined + // in the bundle. + this.knownFileNameToModuleNames.set(filePath, metadata['importAs']); + } if (metadata['metadata']) { // handle direct declarations of the symbol const topLevelSymbolNames = @@ -236,13 +272,6 @@ export class StaticSymbolResolver { const name = unescapeIdentifier(metadataKey); const symbol = this.getStaticSymbol(filePath, name); - let importSymbol: StaticSymbol|undefined = undefined; - if (metadata['importAs']) { - // Index bundle indexes should use the importAs module name instead of a reference - // to the .d.ts file directly. - importSymbol = this.getStaticSymbol(metadata['importAs'], name); - this.recordImportAs(symbol, importSymbol); - } const origin = origins.hasOwnProperty(metadataKey) && origins[metadataKey]; if (origin) { diff --git a/packages/compiler/src/aot/summary_resolver.ts b/packages/compiler/src/aot/summary_resolver.ts index c42e810906..52f7857629 100644 --- a/packages/compiler/src/aot/summary_resolver.ts +++ b/packages/compiler/src/aot/summary_resolver.ts @@ -10,7 +10,7 @@ import {Summary, SummaryResolver} from '../summary_resolver'; import {StaticSymbol, StaticSymbolCache} from './static_symbol'; import {deserializeSummaries} from './summary_serializer'; -import {ngfactoryFilePath, stripNgFactory, summaryFileName} from './util'; +import {ngfactoryFilePath, stripGeneratedFileSuffix, summaryFileName} from './util'; export interface AotSummaryResolverHost { /** @@ -43,7 +43,7 @@ export class AotSummaryResolver implements SummaryResolver { // Note: We need to strip the .ngfactory. file path, // so this method also works for generated files // (for which host.isSourceFile will always return false). - return !this.host.isSourceFile(stripNgFactory(filePath)); + return !this.host.isSourceFile(stripGeneratedFileSuffix(filePath)); } getLibraryFileName(filePath: string) { return this.host.getOutputFileName(filePath); } @@ -68,6 +68,8 @@ export class AotSummaryResolver implements SummaryResolver { return this.importAs.get(staticSymbol) !; } + addSummary(summary: Summary) { this.summaryCache.set(summary.symbol, summary); } + private _loadSummaryFile(filePath: string) { if (this.loadedFilePaths.has(filePath)) { return; diff --git a/packages/compiler/src/aot/summary_serializer.ts b/packages/compiler/src/aot/summary_serializer.ts index 75e0d0b365..8257894469 100644 --- a/packages/compiler/src/aot/summary_serializer.ts +++ b/packages/compiler/src/aot/summary_serializer.ts @@ -5,31 +5,40 @@ * 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 {CompileNgModuleSummary, CompileSummaryKind, CompileTypeSummary} from '../compile_metadata'; +import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileNgModuleMetadata, CompileNgModuleSummary, CompilePipeMetadata, CompileProviderMetadata, CompileSummaryKind, CompileTypeMetadata, CompileTypeSummary} from '../compile_metadata'; +import * as o from '../output/output_ast'; import {Summary, SummaryResolver} from '../summary_resolver'; -import {ValueTransformer, visitValue} from '../util'; +import {ValueTransformer, ValueVisitor, visitValue} from '../util'; import {StaticSymbol, StaticSymbolCache} from './static_symbol'; import {ResolvedStaticSymbol, StaticSymbolResolver} from './static_symbol_resolver'; - +import {summaryForJitFileName, summaryForJitName} from './util'; export function serializeSummaries( summaryResolver: SummaryResolver, symbolResolver: StaticSymbolResolver, - symbols: ResolvedStaticSymbol[], types: CompileTypeSummary[]): - {json: string, exportAs: {symbol: StaticSymbol, exportAs: string}[]} { - const serializer = new Serializer(symbolResolver, summaryResolver); + symbols: ResolvedStaticSymbol[], types: { + summary: CompileTypeSummary, + metadata: CompileNgModuleMetadata | CompileDirectiveMetadata | CompilePipeMetadata | + CompileTypeMetadata + }[]): { + json: string, + exportAs: {symbol: StaticSymbol, exportAs: string}[], + forJit: {statements: o.Statement[], exportedVars: string[]} +} { + const toJsonSerializer = new ToJsonSerializer(symbolResolver, summaryResolver); + const forJitSerializer = new ForJitSerializer(symbolResolver); // for symbols, we use everything except for the class metadata itself // (we keep the statics though), as the class metadata is contained in the // CompileTypeSummary. symbols.forEach( - (resolvedSymbol) => serializer.addOrMergeSummary( + (resolvedSymbol) => toJsonSerializer.addOrMergeSummary( {symbol: resolvedSymbol.symbol, metadata: resolvedSymbol.metadata})); // Add summaries that are referenced by the given symbols (transitively) // Note: the serializer.symbols array might be growing while // we execute the loop! - for (let processedIndex = 0; processedIndex < serializer.symbols.length; processedIndex++) { - const symbol = serializer.symbols[processedIndex]; + for (let processedIndex = 0; processedIndex < toJsonSerializer.symbols.length; processedIndex++) { + const symbol = toJsonSerializer.symbols[processedIndex]; if (summaryResolver.isLibraryFile(symbol.filePath)) { let summary = summaryResolver.resolveSummary(symbol); if (!summary) { @@ -42,7 +51,10 @@ export function serializeSummaries( } } if (summary) { - serializer.addOrMergeSummary(summary); + if (summary.type) { + forJitSerializer.addLibType(summary.type); + } + toJsonSerializer.addOrMergeSummary(summary); } } } @@ -51,32 +63,35 @@ export function serializeSummaries( // Note: We don't add the summaries of all referenced symbols as for the ResolvedSymbols, // as the type summaries already contain the transitive data that they require // (in a minimal way). - types.forEach((typeSummary) => { - serializer.addOrMergeSummary( - {symbol: typeSummary.type.reference, metadata: null, type: typeSummary}); - if (typeSummary.summaryKind === CompileSummaryKind.NgModule) { - const ngModuleSummary = typeSummary; + types.forEach(({summary, metadata}) => { + forJitSerializer.addSourceType(summary, metadata); + toJsonSerializer.addOrMergeSummary( + {symbol: summary.type.reference, metadata: null, type: summary}); + if (summary.summaryKind === CompileSummaryKind.NgModule) { + const ngModuleSummary = summary; ngModuleSummary.exportedDirectives.concat(ngModuleSummary.exportedPipes).forEach((id) => { const symbol: StaticSymbol = id.reference; if (summaryResolver.isLibraryFile(symbol.filePath)) { const summary = summaryResolver.resolveSummary(symbol); if (summary) { - serializer.addOrMergeSummary(summary); + toJsonSerializer.addOrMergeSummary(summary); } } }); } }); - return serializer.serialize(); + const {json, exportAs} = toJsonSerializer.serialize(); + const {statements, exportedVars} = forJitSerializer.serialize(exportAs); + return {json, forJit: {statements, exportedVars}, exportAs}; } export function deserializeSummaries(symbolCache: StaticSymbolCache, json: string): {summaries: Summary[], importAs: {symbol: StaticSymbol, importAs: string}[]} { - const deserializer = new Deserializer(symbolCache); + const deserializer = new FromJsonDeserializer(symbolCache); return deserializer.deserialize(json); } -class Serializer extends ValueTransformer { +class ToJsonSerializer extends ValueTransformer { // Note: This only contains symbols without members. symbols: StaticSymbol[] = []; private indexBySymbol = new Map(); @@ -169,7 +184,132 @@ class Serializer extends ValueTransformer { } } -class Deserializer extends ValueTransformer { +class ForJitSerializer { + private data = new Map(); + + constructor(private symbolResolver: StaticSymbolResolver) {} + + addSourceType( + summary: CompileTypeSummary, metadata: CompileNgModuleMetadata|CompileDirectiveMetadata| + CompilePipeMetadata|CompileTypeMetadata) { + this.data.set(summary.type.reference, {summary, metadata, isLibrary: false}); + } + + addLibType(summary: CompileTypeSummary) { + this.data.set(summary.type.reference, {summary, metadata: null, isLibrary: true}); + } + + serialize(exportAs: {symbol: StaticSymbol, exportAs: string}[]): + {statements: o.Statement[], exportedVars: string[]} { + const statements: o.Statement[] = []; + const exportedVars: string[] = []; + const ngModuleSymbols = new Set(); + + Array.from(this.data.values()).forEach(({summary, metadata, isLibrary}) => { + if (summary.summaryKind === CompileSummaryKind.NgModule) { + // collect the symbols that refer to NgModule classes. + // Note: we can't just rely on `summary.type.summaryKind` to determine this as + // we don't add the summaries of all referenced symbols when we serialize type summaries. + // See serializeSummaries for details. + ngModuleSymbols.add(summary.type.reference); + const modSummary = summary; + modSummary.modules.forEach((mod) => { ngModuleSymbols.add(mod.reference); }); + } + if (!isLibrary) { + const fnName = summaryForJitName(summary.type.reference.name); + statements.push( + o.fn([], [new o.ReturnStatement(this.serializeSummaryWithDeps(summary, metadata !))], + new o.ArrayType(o.DYNAMIC_TYPE)) + .toDeclStmt(fnName, [o.StmtModifier.Final])); + exportedVars.push(fnName); + } + }); + + exportAs.forEach((entry) => { + const symbol = entry.symbol; + if (ngModuleSymbols.has(symbol)) { + const jitExportAsName = summaryForJitName(entry.exportAs); + statements.push( + o.variable(jitExportAsName).set(this.serializeSummaryRef(symbol)).toDeclStmt()); + exportedVars.push(jitExportAsName); + } + }); + + return {statements, exportedVars}; + } + + private serializeSummaryWithDeps( + summary: CompileTypeSummary, metadata: CompileNgModuleMetadata|CompileDirectiveMetadata| + CompilePipeMetadata|CompileTypeMetadata): o.Expression { + const expressions: o.Expression[] = [this.serializeSummary(summary)]; + let providers: CompileProviderMetadata[] = []; + if (metadata instanceof CompileNgModuleMetadata) { + expressions.push(... + // For directives / pipes, we only add the declared ones, + // and rely on transitively importing NgModules to get the transitive + // summaries. + metadata.declaredDirectives.concat(metadata.declaredPipes) + .map(type => type.reference) + // For modules, + // we also add the summaries for modules + // from libraries. + // This is ok as we produce reexports for all transitive modules. + .concat(metadata.transitiveModule.modules.map(type => type.reference) + .filter(ref => ref !== metadata.type.reference)) + .map((ref) => this.serializeSummaryRef(ref))); + // Note: We don't use `NgModuleSummary.providers`, as that one is transitive, + // and we already have transitive modules. + providers = metadata.providers; + } else if (summary.summaryKind === CompileSummaryKind.Directive) { + const dirSummary = summary; + providers = dirSummary.providers.concat(dirSummary.viewProviders); + } + // Note: We can't just refer to the `ngsummary.ts` files for `useClass` providers (as we do for + // declaredDirectives / declaredPipes), as we allow + // providers without ctor arguments to skip the `@Injectable` decorator, + // i.e. we didn't generate .ngsummary.ts files for these. + expressions.push( + ...providers.filter(provider => !!provider.useClass).map(provider => this.serializeSummary({ + summaryKind: CompileSummaryKind.Injectable, type: provider.useClass + } as CompileTypeSummary))); + return o.literalArr(expressions); + } + + private serializeSummaryRef(typeSymbol: StaticSymbol): o.Expression { + const jitImportedSymbol = this.symbolResolver.getStaticSymbol( + summaryForJitFileName(typeSymbol.filePath), summaryForJitName(typeSymbol.name)); + return o.importExpr({reference: jitImportedSymbol}); + } + + private serializeSummary(data: {[key: string]: any}): o.Expression { + class Transformer implements ValueVisitor { + visitArray(arr: any[], context: any): any { + return o.literalArr(arr.map(entry => visitValue(entry, this, context))); + } + visitStringMap(map: {[key: string]: any}, context: any): any { + return new o.LiteralMapExpr(Object.keys(map).map( + (key) => new o.LiteralMapEntry(key, visitValue(map[key], this, context)))); + } + visitPrimitive(value: any, context: any): any { return o.literal(value); } + visitOther(value: any, context: any): any { + if (value instanceof StaticSymbol) { + return o.importExpr({reference: value}); + } else { + throw new Error(`Illegal State: Encountered value ${value}`); + } + } + } + + return visitValue(data, new Transformer(), null); + } +} + +class FromJsonDeserializer extends ValueTransformer { private symbols: StaticSymbol[]; constructor(private symbolCache: StaticSymbolCache) { super(); } diff --git a/packages/compiler/src/aot/util.ts b/packages/compiler/src/aot/util.ts index 4285f805ec..1d02da8d1e 100644 --- a/packages/compiler/src/aot/util.ts +++ b/packages/compiler/src/aot/util.ts @@ -7,24 +7,27 @@ */ const STRIP_SRC_FILE_SUFFIXES = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/; -const NG_FACTORY = /\.ngfactory\./; +const GENERATED_FILE = /\.ngfactory\.|\.ngsummary\./; +const GENERATED_MODULE = /\.ngfactory$|\.ngsummary$/; +const JIT_SUMMARY_FILE = /\.ngsummary\./; +const JIT_SUMMARY_NAME = /NgSummary$/; -export function ngfactoryFilePath(filePath: string): string { - const urlWithSuffix = splitTypescriptSuffix(filePath); +export function ngfactoryFilePath(filePath: string, forceSourceFile = false): string { + const urlWithSuffix = splitTypescriptSuffix(filePath, forceSourceFile); return `${urlWithSuffix[0]}.ngfactory${urlWithSuffix[1]}`; } -export function stripNgFactory(filePath: string): string { - return filePath.replace(NG_FACTORY, '.'); +export function stripGeneratedFileSuffix(filePath: string): string { + return filePath.replace(GENERATED_FILE, '.'); } -export function isNgFactoryFile(filePath: string): boolean { - return NG_FACTORY.test(filePath); +export function isGeneratedFile(filePath: string): boolean { + return GENERATED_FILE.test(filePath); } -export function splitTypescriptSuffix(path: string): string[] { +export function splitTypescriptSuffix(path: string, forceSourceFile = false): string[] { if (path.endsWith('.d.ts')) { - return [path.slice(0, -5), '.ts']; + return [path.slice(0, -5), forceSourceFile ? '.ts' : '.d.ts']; } const lastDot = path.lastIndexOf('.'); @@ -40,3 +43,20 @@ export function summaryFileName(fileName: string): string { const fileNameWithoutSuffix = fileName.replace(STRIP_SRC_FILE_SUFFIXES, ''); return `${fileNameWithoutSuffix}.ngsummary.json`; } + +export function summaryForJitFileName(fileName: string, forceSourceFile = false): string { + const urlWithSuffix = splitTypescriptSuffix(stripGeneratedFileSuffix(fileName), forceSourceFile); + return `${urlWithSuffix[0]}.ngsummary${urlWithSuffix[1]}`; +} + +export function stripSummaryForJitFileSuffix(filePath: string): string { + return filePath.replace(JIT_SUMMARY_FILE, '.'); +} + +export function summaryForJitName(symbolName: string): string { + return `${symbolName}NgSummary`; +} + +export function stripSummaryForJitNameSuffix(symbolName: string): string { + return symbolName.replace(JIT_SUMMARY_NAME, ''); +} \ No newline at end of file diff --git a/packages/compiler/src/jit/compiler.ts b/packages/compiler/src/jit/compiler.ts index 19c1198a3d..094a5a39f6 100644 --- a/packages/compiler/src/jit/compiler.ts +++ b/packages/compiler/src/jit/compiler.ts @@ -8,7 +8,7 @@ import {Compiler, ComponentFactory, Inject, Injector, ModuleWithComponentFactories, NgModuleFactory, Type, ɵConsole as Console, ɵgetComponentViewDefinitionFactory as getComponentViewDefinitionFactory, ɵstringify as stringify} from '@angular/core'; -import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileStylesheetMetadata, ProviderMeta, ProxyClass, createHostComponentMeta, identifierName, ngModuleJitUrl, sharedStylesheetJitUrl, templateJitUrl, templateSourceUrl} from '../compile_metadata'; +import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileStylesheetMetadata, CompileTypeSummary, ProviderMeta, ProxyClass, createHostComponentMeta, identifierName, ngModuleJitUrl, sharedStylesheetJitUrl, templateJitUrl, templateSourceUrl} from '../compile_metadata'; import {CompilerConfig} from '../config'; import {CompilerInjectable} from '../injectable'; import {CompileMetadataResolver} from '../metadata_resolver'; @@ -17,6 +17,7 @@ import * as ir from '../output/output_ast'; import {interpretStatements} from '../output/output_interpreter'; import {jitStatements} from '../output/output_jit'; import {CompiledStylesheet, StyleCompiler} from '../style_compiler'; +import {SummaryResolver} from '../summary_resolver'; import {TemplateParser} from '../template_parser/template_parser'; import {SyncAsyncResult} from '../util'; import {ViewCompiler} from '../view_compiler/view_compiler'; @@ -44,7 +45,8 @@ export class JitCompiler implements Compiler { private _injector: Injector, private _metadataResolver: CompileMetadataResolver, private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler, private _ngModuleCompiler: NgModuleCompiler, - private _compilerConfig: CompilerConfig, private _console: Console) {} + private _summaryResolver: SummaryResolver>, private _compilerConfig: CompilerConfig, + private _console: Console) {} get injector(): Injector { return this._injector; } @@ -75,6 +77,25 @@ export class JitCompiler implements Compiler { return template.compMeta.template !.ngContentSelectors; } + getComponentFactory(component: Type): ComponentFactory { + const summary = this._metadataResolver.getDirectiveSummary(component); + return >summary.componentFactory; + } + + loadAotSummaries(summaries: () => any[]) { + this.clearCache(); + flattenSummaries(summaries).forEach((summary) => { + this._summaryResolver.addSummary( + {symbol: summary.type.reference, metadata: null, type: summary}); + }); + } + + hasAotSummary(ref: Type) { return !!this._summaryResolver.resolveSummary(ref); } + + private _filterJitIdentifiers(ids: CompileIdentifierMetadata[]): any[] { + return ids.map(mod => mod.reference).filter((ref) => !this.hasAotSummary(ref)); + } + private _compileModuleAndComponents(moduleType: Type, isSync: boolean): SyncAsyncResult> { const loadingPromise = this._loadModules(moduleType, isSync); @@ -106,14 +127,21 @@ export class JitCompiler implements Compiler { private _loadModules(mainModule: any, isSync: boolean): Promise { const loadingPromises: Promise[] = []; - 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 loadNgModuleDirectiveAndPipeMetadata for all nested modules. - ngModule.transitiveModule.modules.forEach((localModuleMeta) => { - loadingPromises.push(this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata( - localModuleMeta.reference, isSync)); + const mainNgModule = this._metadataResolver.getNgModuleMetadata(mainModule) !; + // Note: for runtime compilation, we want to transitively compile all modules, + // so we also need to load the declared directives / pipes for all nested modules. + this._filterJitIdentifiers(mainNgModule.transitiveModule.modules).forEach((nestedNgModule) => { + // getNgModuleMetadata only returns null if the value passed in is not an NgModule + const moduleMeta = this._metadataResolver.getNgModuleMetadata(nestedNgModule) !; + this._filterJitIdentifiers(moduleMeta.declaredDirectives).forEach((ref) => { + const promise = + this._metadataResolver.loadDirectiveMetadata(moduleMeta.type.reference, ref, isSync); + if (promise) { + loadingPromises.push(promise); + } + }); + this._filterJitIdentifiers(moduleMeta.declaredPipes) + .forEach((ref) => this._metadataResolver.getOrLoadPipeMetadata(ref)); }); return Promise.all(loadingPromises); } @@ -144,15 +172,15 @@ export class JitCompiler implements Compiler { */ _compileComponents(mainModule: Type, allComponentFactories: ComponentFactory[]|null) { const ngModule = this._metadataResolver.getNgModuleMetadata(mainModule) !; - const moduleByDirective = new Map(); + const moduleByJitDirective = new Map(); const templates = new Set(); - ngModule.transitiveModule.modules.forEach((localModuleSummary) => { - const localModuleMeta = - this._metadataResolver.getNgModuleMetadata(localModuleSummary.reference) !; - localModuleMeta.declaredDirectives.forEach((dirIdentifier) => { - moduleByDirective.set(dirIdentifier.reference, localModuleMeta); - const dirMeta = this._metadataResolver.getDirectiveMetadata(dirIdentifier.reference); + const transJitModules = this._filterJitIdentifiers(ngModule.transitiveModule.modules); + transJitModules.forEach((localMod) => { + const localModuleMeta = this._metadataResolver.getNgModuleMetadata(localMod) !; + this._filterJitIdentifiers(localModuleMeta.declaredDirectives).forEach((dirRef) => { + moduleByJitDirective.set(dirRef, localModuleMeta); + const dirMeta = this._metadataResolver.getDirectiveMetadata(dirRef); if (dirMeta.isComponent) { templates.add(this._createCompiledTemplate(dirMeta, localModuleMeta)); if (allComponentFactories) { @@ -164,23 +192,24 @@ export class JitCompiler implements Compiler { } }); }); - ngModule.transitiveModule.modules.forEach((localModuleSummary) => { - const localModuleMeta = - this._metadataResolver.getNgModuleMetadata(localModuleSummary.reference) !; - localModuleMeta.declaredDirectives.forEach((dirIdentifier) => { - const dirMeta = this._metadataResolver.getDirectiveMetadata(dirIdentifier.reference); + transJitModules.forEach((localMod) => { + const localModuleMeta = this._metadataResolver.getNgModuleMetadata(localMod) !; + this._filterJitIdentifiers(localModuleMeta.declaredDirectives).forEach((dirRef) => { + const dirMeta = this._metadataResolver.getDirectiveMetadata(dirRef); if (dirMeta.isComponent) { dirMeta.entryComponents.forEach((entryComponentType) => { - const moduleMeta = moduleByDirective.get(entryComponentType.componentType) !; + const moduleMeta = moduleByJitDirective.get(entryComponentType.componentType) !; templates.add( this._createCompiledHostTemplate(entryComponentType.componentType, moduleMeta)); }); } }); localModuleMeta.entryComponents.forEach((entryComponentType) => { - const moduleMeta = moduleByDirective.get(entryComponentType.componentType) !; - templates.add( - this._createCompiledHostTemplate(entryComponentType.componentType, moduleMeta)); + if (!this.hasAotSummary(entryComponentType.componentType.reference)) { + const moduleMeta = moduleByJitDirective.get(entryComponentType.componentType) !; + templates.add( + this._createCompiledHostTemplate(entryComponentType.componentType, moduleMeta)); + } }); }); templates.forEach((template) => this._compileTemplate(template)); @@ -353,7 +382,6 @@ class ModuleBoundCompiler implements Compiler { return this._delegate.getNgContentSelectors(component); } - /** * Clears all caches */ @@ -364,3 +392,15 @@ class ModuleBoundCompiler implements Compiler { */ clearCacheFor(type: Type) { this._delegate.clearCacheFor(type); } } + + +function flattenSummaries(fn: () => any[], out: CompileTypeSummary[] = []): CompileTypeSummary[] { + fn().forEach((entry) => { + if (typeof entry === 'function') { + flattenSummaries(entry, out); + } else { + out.push(entry); + } + }); + return out; +} diff --git a/packages/compiler/src/jit/compiler_factory.ts b/packages/compiler/src/jit/compiler_factory.ts index 5a180e1f71..0060bc51a2 100644 --- a/packages/compiler/src/jit/compiler_factory.ts +++ b/packages/compiler/src/jit/compiler_factory.ts @@ -7,6 +7,7 @@ */ import {COMPILER_OPTIONS, Compiler, CompilerFactory, CompilerOptions, Inject, InjectionToken, MissingTranslationStrategy, Optional, PLATFORM_INITIALIZER, PlatformRef, Provider, ReflectiveInjector, TRANSLATIONS, TRANSLATIONS_FORMAT, Type, ViewEncapsulation, createPlatformFactory, isDevMode, platformCore, ɵConsole as Console, ɵReflectionCapabilities as ReflectionCapabilities, ɵReflector as Reflector, ɵReflectorReader as ReflectorReader, ɵreflector as reflector} from '@angular/core'; + import {CompilerConfig} from '../config'; import {DirectiveNormalizer} from '../directive_normalizer'; import {DirectiveResolver} from '../directive_resolver'; @@ -23,7 +24,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 {JitSummaryResolver, 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'; @@ -38,13 +39,6 @@ const _NO_RESOURCE_LOADER: ResourceLoader = { const baseHtmlParser = new InjectionToken('HtmlParser'); -export function i18nHtmlParserFactory( - parser: HtmlParser, translations: string, format: string, config: CompilerConfig, - console: Console): i18n.I18NHtmlParser { - return new i18n.I18NHtmlParser( - parser, translations, format, config.missingTranslation !, console); -} - /** * A set of providers that provide `JitCompiler` and its dependencies to use for * template compilation. @@ -53,7 +47,8 @@ export const COMPILER_PROVIDERS: Array|{[k: string]: any}|any[]> = {provide: Reflector, useValue: reflector}, {provide: ReflectorReader, useExisting: Reflector}, {provide: ResourceLoader, useValue: _NO_RESOURCE_LOADER}, - SummaryResolver, + JitSummaryResolver, + {provide: SummaryResolver, useExisting: JitSummaryResolver}, Console, Lexer, Parser, @@ -63,7 +58,10 @@ export const COMPILER_PROVIDERS: Array|{[k: string]: any}|any[]> = }, { provide: i18n.I18NHtmlParser, - useFactory: i18nHtmlParserFactory, + useFactory: (parser: HtmlParser, translations: string, format: string, config: CompilerConfig, + console: Console) => + new i18n.I18NHtmlParser( + parser, translations, format, config.missingTranslation !, console), deps: [ baseHtmlParser, [new Optional(), new Inject(TRANSLATIONS)], diff --git a/packages/compiler/src/metadata_resolver.ts b/packages/compiler/src/metadata_resolver.ts index 21685a565d..b308d64126 100644 --- a/packages/compiler/src/metadata_resolver.ts +++ b/packages/compiler/src/metadata_resolver.ts @@ -167,8 +167,7 @@ export class CompileMetadataResolver { return typeSummary && typeSummary.summaryKind === kind ? typeSummary : null; } - private _loadDirectiveMetadata(ngModuleType: any, directiveType: any, isSync: boolean): - Promise|null { + loadDirectiveMetadata(ngModuleType: any, directiveType: any, isSync: boolean): Promise|null { if (this._directiveCache.has(directiveType)) { return null; } @@ -377,9 +376,20 @@ export class CompileMetadataResolver { return dirSummary; } - isDirective(type: any) { return this._directiveResolver.isDirective(type); } + isDirective(type: any) { + return !!this._loadSummary(type, cpl.CompileSummaryKind.Directive) || + this._directiveResolver.isDirective(type); + } - isPipe(type: any) { return this._pipeResolver.isPipe(type); } + isPipe(type: any) { + return !!this._loadSummary(type, cpl.CompileSummaryKind.Pipe) || + this._pipeResolver.isPipe(type); + } + + isNgModule(type: any) { + return !!this._loadSummary(type, cpl.CompileSummaryKind.NgModule) || + this._ngModuleResolver.isNgModule(type); + } getNgModuleSummary(moduleType: any): cpl.CompileNgModuleSummary|null { let moduleSummary: cpl.CompileNgModuleSummary|null = @@ -403,7 +413,7 @@ export class CompileMetadataResolver { const loading: Promise[] = []; if (ngModule) { ngModule.declaredDirectives.forEach((id) => { - const promise = this._loadDirectiveMetadata(moduleType, id.reference, isSync); + const promise = this.loadDirectiveMetadata(moduleType, id.reference, isSync); if (promise) { loading.push(promise); } @@ -501,11 +511,11 @@ export class CompileMetadataResolver { return; } const declaredIdentifier = this._getIdentifierMetadata(declaredType); - if (this._directiveResolver.isDirective(declaredType)) { + if (this.isDirective(declaredType)) { transitiveModule.addDirective(declaredIdentifier); declaredDirectives.push(declaredIdentifier); this._addTypeToModule(declaredType, moduleType); - } else if (this._pipeResolver.isPipe(declaredType)) { + } else if (this.isPipe(declaredType)) { transitiveModule.addPipe(declaredIdentifier); transitiveModule.pipes.push(declaredIdentifier); declaredPipes.push(declaredIdentifier); @@ -604,15 +614,15 @@ export class CompileMetadataResolver { } private _getTypeDescriptor(type: Type): string { - if (this._directiveResolver.isDirective(type)) { + if (this.isDirective(type)) { return 'directive'; } - if (this._pipeResolver.isPipe(type)) { + if (this.isPipe(type)) { return 'pipe'; } - if (this._ngModuleResolver.isNgModule(type)) { + if (this.isNgModule(type)) { return 'module'; } diff --git a/packages/compiler/src/summary_resolver.ts b/packages/compiler/src/summary_resolver.ts index d7c16ae0d1..d6246ac9ac 100644 --- a/packages/compiler/src/summary_resolver.ts +++ b/packages/compiler/src/summary_resolver.ts @@ -5,6 +5,7 @@ * 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 {Type} from '@angular/core'; import {CompileTypeSummary} from './compile_metadata'; import {CompilerInjectable} from './injectable'; @@ -14,11 +15,25 @@ export interface Summary { type?: CompileTypeSummary; } +export abstract class SummaryResolver { + abstract isLibraryFile(fileName: string): boolean; + abstract getLibraryFileName(fileName: string): string|null; + abstract resolveSummary(reference: T): Summary|null; + abstract getSymbolsOf(filePath: string): T[]; + abstract getImportAs(reference: T): T; + abstract addSummary(summary: Summary): void; +} + @CompilerInjectable() -export class SummaryResolver { +export class JitSummaryResolver implements SummaryResolver> { + private _summaries = new Map, Summary>>(); + isLibraryFile(fileName: string): boolean { return false; }; getLibraryFileName(fileName: string): string|null { return null; } - resolveSummary(reference: T): Summary|null { return null; }; - getSymbolsOf(filePath: string): T[] { return []; } - getImportAs(reference: T): T { return reference; } + resolveSummary(reference: Type): Summary>|null { + return this._summaries.get(reference) || null; + }; + getSymbolsOf(filePath: string): Type[] { return []; } + getImportAs(reference: Type): Type { return reference; } + addSummary(summary: Summary>) { this._summaries.set(summary.symbol, summary); }; } diff --git a/packages/compiler/test/aot/jit_summaries_spec.ts b/packages/compiler/test/aot/jit_summaries_spec.ts new file mode 100644 index 0000000000..fb7e140003 --- /dev/null +++ b/packages/compiler/test/aot/jit_summaries_spec.ts @@ -0,0 +1,347 @@ +/** + * @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, CompileSummaryKind, GeneratedFile, createAotCompiler} from '@angular/compiler'; +import {fakeAsync, tick} from '@angular/core/testing'; + +import {MockDirectory, compile, setup} from './test_util'; + +describe('aot summaries for jit', () => { + let angularFiles = setup(); + + function compileApp(rootDir: MockDirectory, options: {useSummaries?: boolean} = {}): + {genFiles: GeneratedFile[], outDir: MockDirectory} { + let result: {genFiles: GeneratedFile[], outDir: MockDirectory} = null !; + let error: Error|null = null; + compile([rootDir, angularFiles], options).then((r) => result = r, (e) => error = e); + tick(); + if (error) { + throw error; + } + return result; + } + + it('should create @Injectable summaries', fakeAsync(() => { + const appDir = { + 'app.module.ts': ` + import { Injectable } from '@angular/core'; + + export class Dep {} + + @Injectable() + export class MyService { + constructor(d: Dep) {} + } + ` + }; + const rootDir = {'app': appDir}; + + const genFile = + compileApp(rootDir).genFiles.find(f => f.genFileUrl === '/app/app.module.ngsummary.ts'); + + expect(genFile.source).toContain(`import * as import0 from '/app/app.module'`); + expect(genFile.source).toContain('export function MyServiceNgSummary()'); + // Note: CompileSummaryKind.Injectable = 3 + expect(genFile.source).toMatch(/summaryKind: 3,\s*type: \{\s*reference: import0.MyService/); + expect(genFile.source).toContain('token: {identifier: {reference: import0.Dep}}'); + })); + + it('should create @Pipe summaries', fakeAsync(() => { + const appDir = { + 'app.module.ts': ` + import { Pipe, NgModule } from '@angular/core'; + + export class Dep {} + + @Pipe({name: 'myPipe'}) + export class MyPipe { + constructor(d: Dep) {} + } + + @NgModule({declarations: [MyPipe]}) + export class MyModule {} + ` + }; + const rootDir = {'app': appDir}; + + const genFile = + compileApp(rootDir).genFiles.find(f => f.genFileUrl === '/app/app.module.ngsummary.ts'); + + expect(genFile.source).toContain(`import * as import0 from '/app/app.module'`); + expect(genFile.source).toContain('export function MyPipeNgSummary()'); + // Note: CompileSummaryKind.Pipe = 1 + expect(genFile.source).toMatch(/summaryKind: 0,\s*type: \{\s*reference: import0.MyPipe/); + expect(genFile.source).toContain('token: {identifier: {reference: import0.Dep}}'); + })); + + it('should create @Directive summaries', fakeAsync(() => { + const appDir = { + 'app.module.ts': ` + import { Directive, NgModule } from '@angular/core'; + + export class Dep {} + + @Directive({selector: '[myDir]'}) + export class MyDirective { + constructor(a: Dep) {} + } + + @NgModule({declarations: [MyDirective]}) + export class MyModule {} + ` + }; + const rootDir = {'app': appDir}; + + const genFile = + compileApp(rootDir).genFiles.find(f => f.genFileUrl === '/app/app.module.ngsummary.ts'); + + expect(genFile.source).toContain(`import * as import0 from '/app/app.module'`); + expect(genFile.source).toContain('export function MyDirectiveNgSummary()'); + // Note: CompileSummaryKind.Directive = 1 + expect(genFile.source) + .toMatch(/summaryKind: 1,\s*type: \{\s*reference: import0.MyDirective/); + expect(genFile.source).toContain('token: {identifier: {reference: import0.Dep}}'); + })); + + it('should create @NgModule summaries', fakeAsync(() => { + const appDir = { + 'app.module.ts': ` + import { NgModule } from '@angular/core'; + + export class Dep {} + + @NgModule() + export class MyModule { + constructor(d: Dep) {} + } + ` + }; + const rootDir = {'app': appDir}; + + const genFile = + compileApp(rootDir).genFiles.find(f => f.genFileUrl === '/app/app.module.ngsummary.ts'); + + expect(genFile.source).toContain(`import * as import0 from '/app/app.module'`); + expect(genFile.source).toContain('export function MyModuleNgSummary()'); + // Note: CompileSummaryKind.NgModule = 2 + expect(genFile.source).toMatch(/summaryKind: 2,\s*type: \{\s*reference: import0.MyModule/); + expect(genFile.source).toContain('token: {identifier: {reference: import0.Dep}}'); + })); + + it('should embed useClass provider summaries in @Directive summaries', fakeAsync(() => { + const appDir = { + 'app.service.ts': ` + import { Injectable } from '@angular/core'; + + export class Dep {} + + @Injectable() + export class MyService { + constructor(d: Dep) {} + } + `, + 'app.module.ts': ` + import { Directive, NgModule } from '@angular/core'; + import { MyService } from './app.service'; + + @Directive({ + selector: '[myDir]', + providers: [MyService] + }) + export class MyDirective {} + + @NgModule({declarations: [MyDirective]}) + export class MyModule {} + ` + }; + const rootDir = {'app': appDir}; + + const genFile = + compileApp(rootDir).genFiles.find(f => f.genFileUrl === '/app/app.module.ngsummary.ts'); + + expect(genFile.source).toMatch(/useClass: \{\s*reference: import1.MyService/); + // Note: CompileSummaryKind.Injectable = 3 + expect(genFile.source).toMatch(/summaryKind: 3,\s*type: \{\s*reference: import1.MyService/); + expect(genFile.source).toContain('token: {identifier: {reference: import1.Dep}}'); + })); + + it('should embed useClass provider summaries into @NgModule summaries', fakeAsync(() => { + const appDir = { + 'app.service.ts': ` + import { Injectable } from '@angular/core'; + + export class Dep {} + + @Injectable() + export class MyService { + constructor(d: Dep) {} + } + `, + 'app.module.ts': ` + import { NgModule } from '@angular/core'; + import { MyService } from './app.service'; + + @NgModule({ + providers: [MyService] + }) + export class MyModule {} + ` + }; + const rootDir = {'app': appDir}; + + const genFile = + compileApp(rootDir).genFiles.find(f => f.genFileUrl === '/app/app.module.ngsummary.ts'); + + expect(genFile.source).toMatch(/useClass: \{\s*reference: import1.MyService/); + // Note: CompileSummaryKind.Injectable = 3 + expect(genFile.source).toMatch(/summaryKind: 3,\s*type: \{\s*reference: import1.MyService/); + expect(genFile.source).toContain('token: {identifier: {reference: import1.Dep}}'); + })); + + it('should reference declared @Directive and @Pipe summaries in @NgModule summaries', + fakeAsync(() => { + const appDir = { + 'app.module.ts': ` + import { Directive, Pipe, NgModule } from '@angular/core'; + + @Directive({selector: '[myDir]'}) + export class MyDirective {} + + @Pipe({name: 'myPipe'}) + export class MyPipe {} + + @NgModule({declarations: [MyDirective, MyPipe]}) + export class MyModule {} + ` + }; + const rootDir = {'app': appDir}; + + const genFile = + compileApp(rootDir).genFiles.find(f => f.genFileUrl === '/app/app.module.ngsummary.ts'); + + expect(genFile.source) + .toMatch( + /export function MyModuleNgSummary()[^;]*,\s*MyDirectiveNgSummary,\s*MyPipeNgSummary\s*\]\s*;/); + })); + + it('should reference imported @NgModule summaries in @NgModule summaries', fakeAsync(() => { + const appDir = { + 'app.module.ts': ` + import { NgModule } from '@angular/core'; + + @NgModule() + export class MyImportedModule {} + + @NgModule({imports: [MyImportedModule]}) + export class MyModule {} + ` + }; + const rootDir = {'app': appDir}; + + const genFile = + compileApp(rootDir).genFiles.find(f => f.genFileUrl === '/app/app.module.ngsummary.ts'); + + expect(genFile.source) + .toMatch( + /export function MyModuleNgSummary()[^;]*,\s*MyImportedModuleNgSummary\s*\]\s*;/); + })); + + it('should create and use reexports for imported NgModules ' + + 'accross compilation units', + fakeAsync(() => { + const lib1In = { + 'lib1': { + 'module.ts': ` + import { NgModule } from '@angular/core'; + + @NgModule() + export class Lib1Module {} + `, + 'reexport.ts': ` + import { NgModule } from '@angular/core'; + + @NgModule() + export class ReexportModule {} + + export const reexports: any[] = [ ReexportModule ]; + `, + } + }; + const {outDir: lib2In, genFiles: lib1Gen} = compileApp(lib1In, {useSummaries: true}); + + lib2In['lib2'] = { + 'module.ts': ` + import { NgModule } from '@angular/core'; + import { Lib1Module } from '../lib1/module'; + + @NgModule({ + imports: [Lib1Module] + }) + export class Lib2Module {} + `, + 'reexport.ts': ` + import { reexports as reexports_lib1 } from '../lib1/reexport'; + export const reexports: any[] = [ reexports_lib1 ]; + `, + }; + const {outDir: lib3In, genFiles: lib2Gen} = compileApp(lib2In, {useSummaries: true}); + + const lib2ModuleNgSummary = lib2Gen.find(f => f.genFileUrl === '/lib2/module.ngsummary.ts'); + const lib2ReexportNgSummary = + lib2Gen.find(f => f.genFileUrl === '/lib2/reexport.ngsummary.ts'); + + // ngsummaries should add reexports for imported NgModules from a direct dependency + expect(lib2ModuleNgSummary.source) + .toContain( + `export {Lib1ModuleNgSummary as Lib1Module_1NgSummary} from '/lib1/module.ngsummary'`); + // ngsummaries should add reexports for reexported values from a direct dependency + expect(lib2ReexportNgSummary.source) + .toContain( + `export {ReexportModuleNgSummary as ReexportModule_2NgSummary} from '/lib1/reexport.ngsummary'`); + + lib3In['lib3'] = { + 'module.ts': ` + import { NgModule } from '@angular/core'; + import { Lib2Module } from '../lib2/module'; + import { reexports } from '../lib2/reexport'; + + @NgModule({ + imports: [Lib2Module, reexports] + }) + export class Lib3Module {} + `, + 'reexport.ts': ` + import { reexports as reexports_lib2 } from '../lib2/reexport'; + export const reexports: any[] = [ reexports_lib2 ]; + `, + }; + + const lib3Gen = compileApp(lib3In, {useSummaries: true}).genFiles; + const lib3ModuleNgSummary = lib3Gen.find(f => f.genFileUrl === '/lib3/module.ngsummary.ts'); + const lib3ReexportNgSummary = + lib3Gen.find(f => f.genFileUrl === '/lib3/reexport.ngsummary.ts'); + + // ngsummary.ts files should use the reexported values from direct and deep deps + expect(lib3ModuleNgSummary.source) + .toContain(`import * as import4 from '/lib2/module.ngsummary'`); + expect(lib3ModuleNgSummary.source) + .toContain(`import * as import5 from '/lib2/reexport.ngsummary'`); + expect(lib3ModuleNgSummary.source) + .toMatch( + /export function Lib3ModuleNgSummary()[^;]*,\s*import4.Lib1Module_1NgSummary,\s*import4.Lib2ModuleNgSummary,\s*import5.ReexportModule_2NgSummary\s*\]\s*;/); + + // ngsummaries should add reexports for imported NgModules from a deep dependency + expect(lib3ModuleNgSummary.source) + .toContain( + `export {Lib1Module_1NgSummary as Lib1Module_1NgSummary,Lib2ModuleNgSummary as Lib2Module_2NgSummary} from '/lib2/module.ngsummary'`); + // ngsummaries should add reexports for reexported values from a deep dependency + expect(lib3ReexportNgSummary.source) + .toContain( + `export {ReexportModule_2NgSummary as ReexportModule_3NgSummary} from '/lib2/reexport.ngsummary'`); + })); +}); diff --git a/packages/compiler/test/aot/static_symbol_resolver_spec.ts b/packages/compiler/test/aot/static_symbol_resolver_spec.ts index 76a5771bd6..0827dee33a 100644 --- a/packages/compiler/test/aot/static_symbol_resolver_spec.ts +++ b/packages/compiler/test/aot/static_symbol_resolver_spec.ts @@ -372,7 +372,7 @@ export class MockSummaryResolver implements SummaryResolver { symbol: StaticSymbol, importAs: StaticSymbol }[] = []) {} - + addSummary(summary: Summary) { this.summaries.push(summary); }; resolveSummary(reference: StaticSymbol): Summary { return this.summaries.find(summary => summary.symbol === reference); }; @@ -442,6 +442,10 @@ export class MockStaticSymbolResolverHost implements StaticSymbolResolverHost { return '/tmp/' + modulePath + '.d.ts'; } + fileNameToModuleName(filePath: string, containingFile: string) { + return filePath.replace(/(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/, ''); + } + getMetadataFor(moduleId: string): any { return this._getMetadataFor(moduleId); } private _getMetadataFor(filePath: string): any { diff --git a/packages/compiler/test/aot/summary_resolver_spec.ts b/packages/compiler/test/aot/summary_resolver_spec.ts index d24d499933..c85b4b8129 100644 --- a/packages/compiler/test/aot/summary_resolver_spec.ts +++ b/packages/compiler/test/aot/summary_resolver_spec.ts @@ -27,17 +27,17 @@ export function main() { summaryResolver = new AotSummaryResolver(host, symbolCache); } - function serialize(symbols: ResolvedStaticSymbol[], types: CompileTypeSummary[]): string { + function serialize(symbols: ResolvedStaticSymbol[]): string { // Note: Don't use the top level host / summaryResolver as they might not be created yet const mockSummaryResolver = new MockSummaryResolver([]); const symbolResolver = new StaticSymbolResolver( new MockStaticSymbolResolverHost({}), symbolCache, mockSummaryResolver); - return serializeSummaries(mockSummaryResolver, symbolResolver, symbols, types).json; + return serializeSummaries(mockSummaryResolver, symbolResolver, symbols, []).json; } it('should load serialized summary files', () => { const asymbol = symbolCache.get('/a.d.ts', 'a'); - init({'/a.ngsummary.json': serialize([{symbol: asymbol, metadata: 1}], [])}); + init({'/a.ngsummary.json': serialize([{symbol: asymbol, metadata: 1}])}); expect(summaryResolver.resolveSummary(asymbol)).toEqual({symbol: asymbol, metadata: 1}); }); @@ -51,13 +51,13 @@ export function main() { it('should cache summaries', () => { const asymbol = symbolCache.get('/a.d.ts', 'a'); - init({'/a.ngsummary.json': serialize([{symbol: asymbol, metadata: 1}], [])}); + init({'/a.ngsummary.json': serialize([{symbol: asymbol, metadata: 1}])}); expect(summaryResolver.resolveSummary(asymbol)).toBe(summaryResolver.resolveSummary(asymbol)); }); it('should return all symbols in a summary', () => { const asymbol = symbolCache.get('/a.d.ts', 'a'); - init({'/a.ngsummary.json': serialize([{symbol: asymbol, metadata: 1}], [])}); + init({'/a.ngsummary.json': serialize([{symbol: asymbol, metadata: 1}])}); expect(summaryResolver.getSymbolsOf('/a.d.ts')).toEqual([asymbol]); }); @@ -66,13 +66,13 @@ export function main() { const srcSymbol = symbolCache.get('/src.ts', 'Src'); init({ '/src.ngsummary.json': - serialize([{symbol: srcSymbol, metadata: 1}, {symbol: libSymbol, metadata: 2}], []) + serialize([{symbol: srcSymbol, metadata: 1}, {symbol: libSymbol, metadata: 2}]) }); summaryResolver.getSymbolsOf('/src.d.ts'); expect(summaryResolver.getImportAs(symbolCache.get('/src.d.ts', 'Src'))).toBeFalsy(); expect(summaryResolver.getImportAs(libSymbol)) - .toBe(symbolCache.get('/src.ngfactory.ts', 'Lib_1')); + .toBe(symbolCache.get('/src.ngfactory.d.ts', 'Lib_1')); }); describe('isLibraryFile', () => { diff --git a/packages/compiler/test/aot/summary_serializer_spec.ts b/packages/compiler/test/aot/summary_serializer_spec.ts index 3d6d1ec9b2..377c74fdd6 100644 --- a/packages/compiler/test/aot/summary_serializer_spec.ts +++ b/packages/compiler/test/aot/summary_serializer_spec.ts @@ -66,11 +66,14 @@ export function main() { } } ], - [{ - summaryKind: CompileSummaryKind.Injectable, - type: { - reference: symbolCache.get('/tmp/some_service.ts', 'SomeService'), - } + [{ + summary: { + summaryKind: CompileSummaryKind.Injectable, + type: { + reference: symbolCache.get('/tmp/some_service.ts', 'SomeService'), + } + } as any, + metadata: null as any }]); @@ -103,17 +106,25 @@ export function main() { () => { init(); const externalSerialized = serializeSummaries(summaryResolver, symbolResolver, [], [ - { - summaryKind: CompileSummaryKind.Pipe, - type: { - reference: symbolCache.get('/tmp/external.ts', 'SomeExternalPipe'), - } + { + summary: { + summaryKind: CompileSummaryKind.Pipe, + type: { + reference: symbolCache.get('/tmp/external.ts', 'SomeExternalPipe'), + } + } as any, + metadata: null as any }, - { - summaryKind: CompileSummaryKind.Directive, - type: { - reference: symbolCache.get('/tmp/external.ts', 'SomeExternalDir'), - } + { + summary: { + summaryKind: CompileSummaryKind.Directive, + type: { + reference: symbolCache.get('/tmp/external.ts', 'SomeExternalDir'), + }, + providers: [], + viewProviders: [], + } as any, + metadata: null as any } ]); init({ @@ -121,17 +132,22 @@ export function main() { }); const serialized = serializeSummaries( - summaryResolver, symbolResolver, [], [{ - summaryKind: CompileSummaryKind.NgModule, - type: {reference: symbolCache.get('/tmp/some_module.ts', 'SomeModule')}, - exportedPipes: [ - {reference: symbolCache.get('/tmp/some_pipe.ts', 'SomePipe')}, - {reference: symbolCache.get('/tmp/external.d.ts', 'SomeExternalPipe')} - ], - exportedDirectives: [ - {reference: symbolCache.get('/tmp/some_dir.ts', 'SomeDir')}, - {reference: symbolCache.get('/tmp/external.d.ts', 'SomeExternalDir')} - ] + summaryResolver, symbolResolver, [], [{ + summary: { + summaryKind: CompileSummaryKind.NgModule, + type: {reference: symbolCache.get('/tmp/some_module.ts', 'SomeModule')}, + exportedPipes: [ + {reference: symbolCache.get('/tmp/some_pipe.ts', 'SomePipe')}, + {reference: symbolCache.get('/tmp/external.d.ts', 'SomeExternalPipe')} + ], + exportedDirectives: [ + {reference: symbolCache.get('/tmp/some_dir.ts', 'SomeDir')}, + {reference: symbolCache.get('/tmp/external.d.ts', 'SomeExternalDir')} + ], + providers: [], + modules: [], + }, + metadata: null as any }]); const summaries = deserializeSummaries(symbolCache, serialized.json).summaries; @@ -157,11 +173,14 @@ export function main() { metadata: {__symbolic: 'class'} } ], - [{ - summaryKind: CompileSummaryKind.Injectable, - type: { - reference: symbolCache.get('/tmp/external_svc.ts', 'SomeService'), - } + [{ + summary: { + summaryKind: CompileSummaryKind.Injectable, + type: { + reference: symbolCache.get('/tmp/external_svc.ts', 'SomeService'), + } + } as any, + metadata: null as any }]); init( { diff --git a/packages/compiler/testing/src/testing.ts b/packages/compiler/testing/src/testing.ts index d270cc8ba0..c77be42fd8 100644 --- a/packages/compiler/testing/src/testing.ts +++ b/packages/compiler/testing/src/testing.ts @@ -26,9 +26,9 @@ export * from './directive_resolver_mock'; export * from './ng_module_resolver_mock'; export * from './pipe_resolver_mock'; -import {createPlatformFactory, ModuleWithComponentFactories, Injectable, CompilerOptions, COMPILER_OPTIONS, CompilerFactory, NgModuleFactory, Injector, NgModule, Component, Directive, Pipe, Type, PlatformRef} from '@angular/core'; +import {createPlatformFactory, ModuleWithComponentFactories, Injectable, CompilerOptions, COMPILER_OPTIONS, CompilerFactory, ComponentFactory, NgModuleFactory, Injector, NgModule, Component, Directive, Pipe, Type, PlatformRef, ɵstringify} from '@angular/core'; import {MetadataOverride, ɵTestingCompilerFactory as TestingCompilerFactory, ɵTestingCompiler as TestingCompiler} from '@angular/core/testing'; -import {platformCoreDynamic, JitCompiler, DirectiveResolver, NgModuleResolver, PipeResolver} from '@angular/compiler'; +import {platformCoreDynamic, JitCompiler, DirectiveResolver, NgModuleResolver, PipeResolver, CompileMetadataResolver} from '@angular/compiler'; import {MockDirectiveResolver} from './directive_resolver_mock'; import {MockNgModuleResolver} from './ng_module_resolver_mock'; import {MockPipeResolver} from './pipe_resolver_mock'; @@ -42,7 +42,8 @@ export class TestingCompilerFactoryImpl implements TestingCompilerFactory { const compiler = this._compilerFactory.createCompiler(options); return new TestingCompilerImpl( compiler, compiler.injector.get(MockDirectiveResolver), - compiler.injector.get(MockPipeResolver), compiler.injector.get(MockNgModuleResolver)); + compiler.injector.get(MockPipeResolver), compiler.injector.get(MockNgModuleResolver), + compiler.injector.get(CompileMetadataResolver)); } } @@ -50,7 +51,8 @@ export class TestingCompilerImpl implements TestingCompiler { private _overrider = new MetadataOverrider(); constructor( private _compiler: JitCompiler, private _directiveResolver: MockDirectiveResolver, - private _pipeResolver: MockPipeResolver, private _moduleResolver: MockNgModuleResolver) {} + private _pipeResolver: MockPipeResolver, private _moduleResolver: MockNgModuleResolver, + private _metadataResolver: CompileMetadataResolver) {} get injector(): Injector { return this._compiler.injector; } compileModuleSync(moduleType: Type): NgModuleFactory { @@ -73,25 +75,40 @@ export class TestingCompilerImpl implements TestingCompiler { return this._compiler.getNgContentSelectors(component); } + getComponentFactory(component: Type): ComponentFactory { + return this._compiler.getComponentFactory(component); + } + + checkOverrideAllowed(type: Type) { + if (this._compiler.hasAotSummary(type)) { + throw new Error(`${ɵstringify(type)} was AOT compiled, so its metadata cannot be changed.`); + } + } + overrideModule(ngModule: Type, override: MetadataOverride): void { + this.checkOverrideAllowed(ngModule); const oldMetadata = this._moduleResolver.resolve(ngModule, false); this._moduleResolver.setNgModule( ngModule, this._overrider.overrideMetadata(NgModule, oldMetadata, override)); } overrideDirective(directive: Type, override: MetadataOverride): void { + this.checkOverrideAllowed(directive); const oldMetadata = this._directiveResolver.resolve(directive, false); this._directiveResolver.setDirective( directive, this._overrider.overrideMetadata(Directive, oldMetadata !, override)); } overrideComponent(component: Type, override: MetadataOverride): void { + this.checkOverrideAllowed(component); const oldMetadata = this._directiveResolver.resolve(component, false); this._directiveResolver.setDirective( component, this._overrider.overrideMetadata(Component, oldMetadata !, override)); } overridePipe(pipe: Type, override: MetadataOverride): void { + this.checkOverrideAllowed(pipe); const oldMetadata = this._pipeResolver.resolve(pipe, false); this._pipeResolver.setPipe(pipe, this._overrider.overrideMetadata(Pipe, oldMetadata, override)); } + loadAotSummaries(summaries: () => any[]) { this._compiler.loadAotSummaries(summaries); } clearCache(): void { this._compiler.clearCache(); } clearCacheFor(type: Type) { this._compiler.clearCacheFor(type); } } diff --git a/packages/core/test/linker/jit_summaries_integration_spec.ts b/packages/core/test/linker/jit_summaries_integration_spec.ts new file mode 100644 index 0000000000..aaea39659c --- /dev/null +++ b/packages/core/test/linker/jit_summaries_integration_spec.ts @@ -0,0 +1,191 @@ +/** + * @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 {ResourceLoader} from '@angular/compiler'; +import {CompileMetadataResolver} from '@angular/compiler/src/metadata_resolver'; +import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock'; +import {Component, Directive, Injectable, NgModule, Pipe, Type} from '@angular/core'; +import {TestBed, async, getTestBed} from '@angular/core/testing'; + +export function main() { + describe('Jit Summaries', () => { + let instances: Map; + + class SomeDep {} + + class Base { + static annotations: any[]; + static parameters: any[][]; + + constructor(public dep: SomeDep) { + instances.set(Object.getPrototypeOf(this).constructor, this); + } + } + + function expectInstanceCreated(type: any) { + const instance = instances.get(type) !; + expect(instance).toBeDefined(); + expect(instance.dep instanceof SomeDep).toBe(true); + } + + class SomeModule extends Base {} + + class SomePrivateComponent extends Base {} + + class SomePublicComponent extends Base {} + + class SomeDirective extends Base {} + + class SomePipe extends Base { + transform(value: any) { return value; } + } + + class SomeService extends Base {} + + function resetTestEnvironmentWithSummaries(summaries?: () => any[]) { + const {platform, ngModule} = getTestBed(); + TestBed.resetTestEnvironment(); + TestBed.initTestEnvironment(ngModule, platform, summaries); + } + + function createSummaries() { + const resourceLoader = new MockResourceLoader(); + + setMetadata(resourceLoader); + + TestBed.configureCompiler({providers: [{provide: ResourceLoader, useValue: resourceLoader}]}); + TestBed.configureTestingModule({imports: [SomeModule], providers: [SomeDep]}); + + TestBed.compileComponents().then(() => { + const metadataResolver = TestBed.get(CompileMetadataResolver) as CompileMetadataResolver; + const summaries = [ + metadataResolver.getNgModuleSummary(SomeModule), + // test nesting via closures, as we use this in the generated code too. + () => + [metadataResolver.getDirectiveSummary(SomePublicComponent), + metadataResolver.getDirectiveSummary(SomePrivateComponent), + ], + metadataResolver.getDirectiveSummary(SomeDirective), + metadataResolver.getPipeSummary(SomePipe), + metadataResolver.getInjectableSummary(SomeService) + ]; + clearMetadata(); + resetTestEnvironmentWithSummaries(() => summaries); + }); + + resourceLoader.flush(); + } + + function setMetadata(resourceLoader: MockResourceLoader) { + Base.parameters = [[SomeDep]]; + + SomeModule.annotations = [new NgModule({ + declarations: [SomePublicComponent, SomePrivateComponent, SomeDirective, SomePipe], + exports: [SomeDirective, SomePipe, SomePublicComponent], + providers: [SomeService] + })]; + + SomePublicComponent.annotations = [new Component({templateUrl: 'somePublicUrl.html'})]; + resourceLoader.expect('somePublicUrl.html', `Hello public world!`); + + SomePrivateComponent.annotations = [new Component({templateUrl: 'somePrivateUrl.html'})]; + resourceLoader.expect('somePrivateUrl.html', `Hello private world!`); + + SomeDirective.annotations = [new Directive({selector: '[someDir]'})]; + + SomePipe.annotations = [new Pipe({name: 'somePipe'})]; + + SomeService.annotations = [new Injectable()]; + } + + function clearMetadata() { + Base.parameters = []; + SomeModule.annotations = []; + SomePublicComponent.annotations = []; + SomePrivateComponent.annotations = []; + SomeDirective.annotations = []; + SomePipe.annotations = []; + SomeService.annotations = []; + } + + beforeEach(async(() => { + instances = new Map(); + createSummaries(); + })); + + afterEach(() => { resetTestEnvironmentWithSummaries(); }); + + it('should use directive metadata from summaries', () => { + @Component({template: '
'}) + class TestComp { + } + + TestBed + .configureTestingModule({providers: [SomeDep], declarations: [TestComp, SomeDirective]}) + .createComponent(TestComp); + expectInstanceCreated(SomeDirective); + }); + + it('should use pipe metadata from summaries', () => { + @Component({template: '{{1 | somePipe}}'}) + class TestComp { + } + + TestBed.configureTestingModule({providers: [SomeDep], declarations: [TestComp, SomePipe]}) + .createComponent(TestComp); + expectInstanceCreated(SomePipe); + }); + + it('should use Service metadata from summaries', () => { + TestBed.configureTestingModule({ + providers: [SomeService, SomeDep], + }); + TestBed.get(SomeService); + expectInstanceCreated(SomeService); + }); + + it('should use NgModule metadata from summaries', () => { + @Component({template: '
{{1 | somePipe}}
'}) + class TestComp { + constructor(service: SomeService) {} + } + + TestBed + .configureTestingModule( + {providers: [SomeDep], declarations: [TestComp], imports: [SomeModule]}) + .createComponent(TestComp); + + expectInstanceCreated(SomeModule); + expectInstanceCreated(SomeDirective); + expectInstanceCreated(SomePipe); + expectInstanceCreated(SomeService); + }); + + it('should allow to create private components from imported NgModule summaries', () => { + TestBed.configureTestingModule({providers: [SomeDep], imports: [SomeModule]}) + .createComponent(SomePrivateComponent); + expectInstanceCreated(SomePrivateComponent); + }); + + it('should throw when trying to mock a type with a summary', () => { + TestBed.resetTestingModule(); + expect(() => TestBed.overrideComponent(SomePrivateComponent, {add: {}}).compileComponents()) + .toThrowError( + 'SomePrivateComponent was AOT compiled, so its metadata cannot be changed.'); + TestBed.resetTestingModule(); + expect(() => TestBed.overrideDirective(SomeDirective, {add: {}}).compileComponents()) + .toThrowError('SomeDirective was AOT compiled, so its metadata cannot be changed.'); + TestBed.resetTestingModule(); + expect(() => TestBed.overridePipe(SomePipe, {add: {name: 'test'}}).compileComponents()) + .toThrowError('SomePipe was AOT compiled, so its metadata cannot be changed.'); + TestBed.resetTestingModule(); + expect(() => TestBed.overrideModule(SomeModule, {add: {}}).compileComponents()) + .toThrowError('SomeModule was AOT compiled, so its metadata cannot be changed.'); + }); + }); +} \ No newline at end of file diff --git a/packages/core/testing/src/test_bed.ts b/packages/core/testing/src/test_bed.ts index 674694a591..677340e15e 100644 --- a/packages/core/testing/src/test_bed.ts +++ b/packages/core/testing/src/test_bed.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompilerOptions, Component, Directive, InjectionToken, Injector, ModuleWithComponentFactories, NgModule, NgModuleRef, NgZone, Pipe, PlatformRef, Provider, ReflectiveInjector, SchemaMetadata, Type, ɵERROR_COMPONENT_TYPE, ɵstringify as stringify} from '@angular/core'; +import {CompilerOptions, Component, Directive, InjectionToken, Injector, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgModuleRef, NgZone, Pipe, PlatformRef, Provider, ReflectiveInjector, SchemaMetadata, Type, ɵERROR_COMPONENT_TYPE, ɵstringify as stringify} from '@angular/core'; + import {AsyncTestCompleter} from './async_test_completer'; import {ComponentFixture} from './component_fixture'; import {MetadataOverride} from './metadata_override'; @@ -69,9 +70,10 @@ export class TestBed implements Injector { * * @experimental */ - static initTestEnvironment(ngModule: Type|Type[], platform: PlatformRef): TestBed { + static initTestEnvironment( + ngModule: Type|Type[], platform: PlatformRef, aotSummaries?: () => any[]): TestBed { const testBed = getTestBed(); - testBed.initTestEnvironment(ngModule, platform); + testBed.initTestEnvironment(ngModule, platform, aotSummaries); return testBed; } @@ -151,7 +153,7 @@ export class TestBed implements Injector { private _compiler: TestingCompiler = null !; private _moduleRef: NgModuleRef = null !; - private _moduleWithComponentFactories: ModuleWithComponentFactories = null !; + private _moduleFactory: NgModuleFactory = null !; private _compilerOptions: CompilerOptions[] = []; @@ -166,6 +168,12 @@ export class TestBed implements Injector { private _schemas: Array = []; private _activeFixtures: ComponentFixture[] = []; + private _aotSummaries: () => any[] = () => []; + + platform: PlatformRef = null !; + + ngModule: Type|Type[] = null !; + /** * Initialize the environment for testing with a compiler factory, a PlatformRef, and an * angular module. These are common to every test in the suite. @@ -179,12 +187,16 @@ export class TestBed implements Injector { * * @experimental */ - initTestEnvironment(ngModule: Type|Type[], platform: PlatformRef) { + initTestEnvironment( + ngModule: Type|Type[], platform: PlatformRef, aotSummaries?: () => any[]) { if (this.platform || this.ngModule) { throw new Error('Cannot set base providers because it has already been called'); } this.platform = platform; this.ngModule = ngModule; + if (aotSummaries) { + this._aotSummaries = aotSummaries; + } } /** @@ -196,6 +208,7 @@ export class TestBed implements Injector { this.resetTestingModule(); this.platform = null !; this.ngModule = null !; + this._aotSummaries = () => []; } resetTestingModule() { @@ -206,7 +219,7 @@ export class TestBed implements Injector { this._pipeOverrides = []; this._moduleRef = null !; - this._moduleWithComponentFactories = null !; + this._moduleFactory = null !; this._compilerOptions = []; this._providers = []; this._declarations = []; @@ -223,10 +236,6 @@ export class TestBed implements Injector { this._activeFixtures = []; } - platform: PlatformRef = null !; - - ngModule: Type|Type[] = null !; - configureCompiler(config: {providers?: any[], useJit?: boolean}) { this._assertNotInstantiated('TestBed.configureCompiler', 'configure the compiler'); this._compilerOptions.push(config); @@ -249,14 +258,14 @@ export class TestBed implements Injector { } compileComponents(): Promise { - if (this._moduleWithComponentFactories || this._instantiated) { + if (this._moduleFactory || this._instantiated) { return Promise.resolve(null); } const moduleType = this._createCompilerAndModule(); return this._compiler.compileModuleAndAllComponentsAsync(moduleType) .then((moduleAndComponentFactories) => { - this._moduleWithComponentFactories = moduleAndComponentFactories; + this._moduleFactory = moduleAndComponentFactories.ngModuleFactory; }); } @@ -264,11 +273,11 @@ export class TestBed implements Injector { if (this._instantiated) { return; } - if (!this._moduleWithComponentFactories) { + if (!this._moduleFactory) { try { const moduleType = this._createCompilerAndModule(); - this._moduleWithComponentFactories = - this._compiler.compileModuleAndAllComponentsSync(moduleType); + this._moduleFactory = + this._compiler.compileModuleAndAllComponentsSync(moduleType).ngModuleFactory; } catch (e) { if (getComponentType(e)) { throw new Error( @@ -282,7 +291,7 @@ export class TestBed implements Injector { const ngZone = new NgZone({enableLongStackTrace: true}); const ngZoneInjector = ReflectiveInjector.resolveAndCreate( [{provide: NgZone, useValue: ngZone}], this.platform.injector); - this._moduleRef = this._moduleWithComponentFactories.ngModuleFactory.create(ngZoneInjector); + this._moduleRef = this._moduleFactory.create(ngZoneInjector); this._instantiated = true; } @@ -300,6 +309,7 @@ export class TestBed implements Injector { this.platform.injector.get(TestingCompilerFactory); this._compiler = compilerFactory.createTestingCompiler(this._compilerOptions.concat([{useDebug: true}])); + this._compiler.loadAotSummaries(this._aotSummaries); this._moduleOverrides.forEach((entry) => this._compiler.overrideModule(entry[0], entry[1])); this._componentOverrides.forEach( (entry) => this._compiler.overrideComponent(entry[0], entry[1])); @@ -356,8 +366,7 @@ export class TestBed implements Injector { createComponent(component: Type): ComponentFixture { this._initIfNeeded(); - const componentFactory = this._moduleWithComponentFactories.componentFactories.find( - (compFactory) => compFactory.componentType === component); + const componentFactory = this._compiler.getComponentFactory(component); if (!componentFactory) { throw new Error( diff --git a/packages/core/testing/src/test_compiler.ts b/packages/core/testing/src/test_compiler.ts index c91c43fe74..f2dd3d474f 100644 --- a/packages/core/testing/src/test_compiler.ts +++ b/packages/core/testing/src/test_compiler.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Compiler, CompilerOptions, Component, Directive, Injector, NgModule, Pipe, Type} from '@angular/core'; +import {Compiler, CompilerOptions, Component, ComponentFactory, Directive, Injector, NgModule, Pipe, Type} from '@angular/core'; import {MetadataOverride} from './metadata_override'; @@ -33,6 +33,18 @@ export class TestingCompiler extends Compiler { overridePipe(directive: Type, overrides: MetadataOverride): void { throw unimplemented(); } + /** + * Allows to pass the compile summary from AOT compilation to the JIT compiler, + * so that it can use the code generated by AOT. + */ + loadAotSummaries(summaries: () => any[]) { throw unimplemented(); }; + + /** + * Gets the component factory for the given component. + * This assumes that the component has been compiled before calling this call using + * `compileModuleAndAllComponents*`. + */ + getComponentFactory(component: Type): ComponentFactory { throw unimplemented(); } } /** diff --git a/packages/language-service/src/typescript_host.ts b/packages/language-service/src/typescript_host.ts index f6e249c728..3278288dbf 100644 --- a/packages/language-service/src/typescript_host.ts +++ b/packages/language-service/src/typescript_host.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, InterpolationConfig, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticAndDynamicReflectionCapabilities, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, analyzeNgModules, componentModuleUrl, createOfflineCompileUrlResolver, extractProgramSymbols} from '@angular/compiler'; +import {AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, InterpolationConfig, JitSummaryResolver, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticAndDynamicReflectionCapabilities, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, analyzeNgModules, componentModuleUrl, createOfflineCompileUrlResolver, extractProgramSymbols} from '@angular/compiler'; import {AngularCompilerOptions} from '@angular/compiler-cli'; import {ViewEncapsulation, ɵConsole as Console} from '@angular/core'; import * as fs from 'fs'; @@ -118,7 +118,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost { new DirectiveNormalizer(resourceLoader, urlResolver, htmlParser, config); result = this._resolver = new CompileMetadataResolver( - config, moduleResolver, directiveResolver, pipeResolver, new SummaryResolver(), + config, moduleResolver, directiveResolver, pipeResolver, new JitSummaryResolver(), elementSchemaRegistry, directiveNormalizer, new Console(), this._staticSymbolCache, this.reflector, (error, type) => this.collectError(error, type && type.filePath)); } diff --git a/tools/public_api_guard/core/testing.d.ts b/tools/public_api_guard/core/testing.d.ts index bf95c891ce..314d138cc9 100644 --- a/tools/public_api_guard/core/testing.d.ts +++ b/tools/public_api_guard/core/testing.d.ts @@ -69,7 +69,7 @@ export declare class TestBed implements Injector { createComponent(component: Type): ComponentFixture; execute(tokens: any[], fn: Function, context?: any): any; get(token: any, notFoundValue?: any): any; - /** @experimental */ initTestEnvironment(ngModule: Type | Type[], platform: PlatformRef): void; + /** @experimental */ initTestEnvironment(ngModule: Type | Type[], platform: PlatformRef, aotSummaries?: () => any[]): void; overrideComponent(component: Type, override: MetadataOverride): void; overrideDirective(directive: Type, override: MetadataOverride): void; overrideModule(ngModule: Type, override: MetadataOverride): void; @@ -84,7 +84,7 @@ export declare class TestBed implements Injector { static configureTestingModule(moduleDef: TestModuleMetadata): typeof TestBed; static createComponent(component: Type): ComponentFixture; static get(token: any, notFoundValue?: any): any; - /** @experimental */ static initTestEnvironment(ngModule: Type | Type[], platform: PlatformRef): TestBed; + /** @experimental */ static initTestEnvironment(ngModule: Type | Type[], platform: PlatformRef, aotSummaries?: () => any[]): TestBed; static overrideComponent(component: Type, override: MetadataOverride): typeof TestBed; static overrideDirective(directive: Type, override: MetadataOverride): typeof TestBed; static overrideModule(ngModule: Type, override: MetadataOverride): typeof TestBed;