diff --git a/packages/compiler-cli/src/codegen.ts b/packages/compiler-cli/src/codegen.ts index d84abbfd29a47..56802aeaf1524 100644 --- a/packages/compiler-cli/src/codegen.ts +++ b/packages/compiler-cli/src/codegen.ts @@ -38,8 +38,9 @@ export class CodeGenerator { codegen(): Promise { return this.compiler - .compileAllAsync(this.program.getSourceFiles().map( + .analyzeModulesAsync(this.program.getSourceFiles().map( sf => this.ngCompilerHost.getCanonicalFileName(sf.fileName))) + .then(analyzedModules => this.compiler.emitAllImpls(analyzedModules)) .then(generatedModules => { generatedModules.forEach(generatedModule => { const sourceFile = this.program.getSourceFile(generatedModule.srcFileUrl); diff --git a/packages/compiler/src/aot/compiler.ts b/packages/compiler/src/aot/compiler.ts index 4a1c854e94749..ae23a5c3d331d 100644 --- a/packages/compiler/src/aot/compiler.ts +++ b/packages/compiler/src/aot/compiler.ts @@ -24,7 +24,7 @@ import {GeneratedFile} from './generated_file'; import {StaticReflector} from './static_reflector'; import {StaticSymbol} from './static_symbol'; import {ResolvedStaticSymbol, StaticSymbolResolver} from './static_symbol_resolver'; -import {serializeSummaries} from './summary_serializer'; +import {createForJitStub, serializeSummaries} from './summary_serializer'; import {ngfactoryFilePath, splitTypescriptSuffix, summaryFileName, summaryForJitFileName, summaryForJitName} from './util'; export class AotCompiler { @@ -39,38 +39,86 @@ export class AotCompiler { clearCache() { this._metadataResolver.clearCache(); } - compileAllAsync(rootFiles: string[]): Promise { + analyzeModulesSync(rootFiles: string[]): NgAnalyzedModules { const programSymbols = extractProgramSymbols(this._symbolResolver, rootFiles, this._host); - const {ngModuleByPipeOrDirective, files, ngModules} = + const analyzeResult = + analyzeAndValidateNgModules(programSymbols, this._host, this._metadataResolver); + analyzeResult.ngModules.forEach( + ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata( + ngModule.type.reference, true)); + return analyzeResult; + } + + analyzeModulesAsync(rootFiles: string[]): Promise { + const programSymbols = extractProgramSymbols(this._symbolResolver, rootFiles, this._host); + const analyzeResult = analyzeAndValidateNgModules(programSymbols, this._host, this._metadataResolver); return Promise - .all(ngModules.map( + .all(analyzeResult.ngModules.map( ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata( ngModule.type.reference, false))) - .then(() => { - const sourceModules = files.map( - file => this._compileSrcFile( - file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.pipes, - file.ngModules, file.injectables)); - return flatten(sourceModules); - }); + .then(() => analyzeResult); } - compileAllSync(rootFiles: string[]): GeneratedFile[] { - const programSymbols = extractProgramSymbols(this._symbolResolver, rootFiles, this._host); - const {ngModuleByPipeOrDirective, files, ngModules} = - analyzeAndValidateNgModules(programSymbols, this._host, this._metadataResolver); - ngModules.forEach( - ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata( - ngModule.type.reference, true)); + emitAllStubs(analyzeResult: NgAnalyzedModules): GeneratedFile[] { + const {files} = analyzeResult; + const sourceModules = + files.map(file => this._compileStubFile(file.srcUrl, file.directives, file.ngModules)); + return flatten(sourceModules); + } + + emitAllImpls(analyzeResult: NgAnalyzedModules): GeneratedFile[] { + const {ngModuleByPipeOrDirective, files} = analyzeResult; const sourceModules = files.map( - file => this._compileSrcFile( + file => this._compileImplFile( file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.pipes, file.ngModules, file.injectables)); return flatten(sourceModules); } - private _compileSrcFile( + private _compileStubFile( + srcFileUrl: string, directives: StaticSymbol[], ngModules: StaticSymbol[]): GeneratedFile[] { + const fileSuffix = splitTypescriptSuffix(srcFileUrl, true)[1]; + const generatedFiles: GeneratedFile[] = []; + + const jitSummaryStmts: o.Statement[] = []; + const ngFactoryStms: o.Statement[] = []; + + const ngFactoryOutputCtx = this._createOutputContext(ngfactoryFilePath(srcFileUrl, true)); + const jitSummaryOutputCtx = this._createOutputContext(summaryForJitFileName(srcFileUrl, true)); + + // create exports that user code can reference + ngModules.forEach((ngModuleReference) => { + this._ngModuleCompiler.createStub(ngFactoryOutputCtx, ngModuleReference); + createForJitStub(jitSummaryOutputCtx, ngModuleReference); + }); + // Note: we are creating stub ngfactory/ngsummary for all source files, + // as the real calculation requires almost the same logic as producing the real content for + // them. + // Our pipeline will filter out empty ones at the end. + generatedFiles.push(this._codegenSourceModule(srcFileUrl, ngFactoryOutputCtx)); + generatedFiles.push(this._codegenSourceModule(srcFileUrl, jitSummaryOutputCtx)); + + // create stubs for external stylesheets (always empty, as users should not import anything from + // the generated code) + directives.forEach((dirType) => { + const compMeta = this._metadataResolver.getDirectiveMetadata(dirType); + if (!compMeta.isComponent) { + return; + } + // Note: compMeta is a component and therefore template is non null. + compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => { + generatedFiles.push(this._codegenSourceModule( + stylesheetMeta.moduleUrl !, + this._createOutputContext(_stylesModuleUrl( + stylesheetMeta.moduleUrl !, this._styleCompiler.needsStyleShim(compMeta), + fileSuffix)))); + }); + }); + return generatedFiles; + } + + private _compileImplFile( srcFileUrl: string, ngModuleByPipeOrDirective: Map, directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: StaticSymbol[], injectables: StaticSymbol[]): GeneratedFile[] { @@ -89,7 +137,7 @@ export class AotCompiler { directives.forEach((dirType) => { const compMeta = this._metadataResolver.getDirectiveMetadata(dirType); if (!compMeta.isComponent) { - return Promise.resolve(null); + return; } const ngModule = ngModuleByPipeOrDirective.get(dirType); if (!ngModule) { @@ -97,13 +145,12 @@ export class AotCompiler { `Internal Error: cannot determine the module for component ${identifierName(compMeta.type)}!`); } - _assertComponent(compMeta); - // compile styles const componentStylesheet = this._styleCompiler.compileComponent(outputCtx, compMeta); // Note: compMeta is a component and therefore template is non null. compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => { - generatedFiles.push(this._codegenStyles(srcFileUrl, compMeta, stylesheetMeta, fileSuffix)); + generatedFiles.push( + this._codegenStyles(stylesheetMeta.moduleUrl !, compMeta, stylesheetMeta, fileSuffix)); }); // compile components @@ -149,7 +196,6 @@ export class AotCompiler { })) ]; const forJitOutputCtx = this._createOutputContext(summaryForJitFileName(srcFileUrl, true)); - const forJitTargetFilePath = summaryForJitFileName(srcFileUrl, true); const {json, exportAs} = serializeSummaries( forJitOutputCtx, this._summaryResolver, this._symbolResolver, symbolSummaries, typeData); exportAs.forEach((entry) => { @@ -305,13 +351,6 @@ function _stylesModuleUrl(stylesheetUrl: string, shim: boolean, suffix: string): return `${stylesheetUrl}${shim ? '.shim' : ''}.ngstyle${suffix}`; } -function _assertComponent(meta: CompileDirectiveMetadata) { - if (!meta.isComponent) { - throw new Error( - `Could not compile '${identifierName(meta.type)}' because it is not a component.`); - } -} - export interface NgAnalyzedModules { ngModules: CompileNgModuleMetadata[]; ngModuleByPipeOrDirective: Map; diff --git a/packages/compiler/src/aot/summary_serializer.ts b/packages/compiler/src/aot/summary_serializer.ts index d6c64e6ba19e5..4bed3c0931454 100644 --- a/packages/compiler/src/aot/summary_serializer.ts +++ b/packages/compiler/src/aot/summary_serializer.ts @@ -87,6 +87,20 @@ export function deserializeSummaries(symbolCache: StaticSymbolCache, json: strin return deserializer.deserialize(json); } +export function createForJitStub(outputCtx: OutputContext, reference: StaticSymbol) { + return createSummaryForJitFunction(outputCtx, reference, o.NULL_EXPR); +} + +function createSummaryForJitFunction( + outputCtx: OutputContext, reference: StaticSymbol, value: o.Expression) { + const fnName = summaryForJitName(reference.name); + outputCtx.statements.push( + o.fn([], [new o.ReturnStatement(value)], new o.ArrayType(o.DYNAMIC_TYPE)).toDeclStmt(fnName, [ + o.StmtModifier.Final, o.StmtModifier.Exported + ])); +} + + class ToJsonSerializer extends ValueTransformer { // Note: This only contains symbols without members. symbols: StaticSymbol[] = []; @@ -215,10 +229,9 @@ class ForJitSerializer { } if (!isLibrary) { const fnName = summaryForJitName(summary.type.reference.name); - this.outputCtx.statements.push( - o.fn([], [new o.ReturnStatement(this.serializeSummaryWithDeps(summary, metadata !))], - new o.ArrayType(o.DYNAMIC_TYPE)) - .toDeclStmt(fnName, [o.StmtModifier.Final, o.StmtModifier.Exported])); + createSummaryForJitFunction( + this.outputCtx, summary.type.reference, + this.serializeSummaryWithDeps(summary, metadata !)); } }); diff --git a/packages/compiler/src/ng_module_compiler.ts b/packages/compiler/src/ng_module_compiler.ts index 3f9f7ae1ce09e..9b576f1a25b7d 100644 --- a/packages/compiler/src/ng_module_compiler.ts +++ b/packages/compiler/src/ng_module_compiler.ts @@ -50,21 +50,13 @@ export class NgModuleCompiler { [new o.FnParam(LOG_VAR.name !)], [new o.ReturnStatement(ngModuleDef)], o.INFERRED_TYPE); const ngModuleFactoryVar = `${identifierName(ngModuleMeta.type)}NgFactory`; - const ngModuleFactoryStmt = - o.variable(ngModuleFactoryVar) - .set(o.importExpr(Identifiers.createModuleFactory).callFn([ - ctx.importExpr(ngModuleMeta.type.reference), - o.literalArr(bootstrapComponents.map(id => ctx.importExpr(id.reference))), - ngModuleDefFactory - ])) - .toDeclStmt( - o.importType( - Identifiers.NgModuleFactory, - [o.expressionType(ctx.importExpr(ngModuleMeta.type.reference)) !], - [o.TypeModifier.Const]), - [o.StmtModifier.Final, o.StmtModifier.Exported]); + this._createNgModuleFactory( + ctx, ngModuleMeta.type.reference, o.importExpr(Identifiers.createModuleFactory).callFn([ + ctx.importExpr(ngModuleMeta.type.reference), + o.literalArr(bootstrapComponents.map(id => ctx.importExpr(id.reference))), + ngModuleDefFactory + ])); - ctx.statements.push(ngModuleFactoryStmt); if (ngModuleMeta.id) { const registerFactoryStmt = o.importExpr(Identifiers.RegisterModuleFactoryFn) @@ -75,4 +67,22 @@ export class NgModuleCompiler { return new NgModuleCompileResult(ngModuleFactoryVar); } + + createStub(ctx: OutputContext, ngModuleReference: any) { + this._createNgModuleFactory(ctx, ngModuleReference, o.NULL_EXPR); + } + + private _createNgModuleFactory(ctx: OutputContext, reference: any, value: o.Expression) { + const ngModuleFactoryVar = `${identifierName({reference: reference})}NgFactory`; + const ngModuleFactoryStmt = + o.variable(ngModuleFactoryVar) + .set(value) + .toDeclStmt( + o.importType( + Identifiers.NgModuleFactory, [o.expressionType(ctx.importExpr(reference)) !], + [o.TypeModifier.Const]), + [o.StmtModifier.Final, o.StmtModifier.Exported]); + + ctx.statements.push(ngModuleFactoryStmt); + } } diff --git a/packages/compiler/test/aot/emit_stubs_spec.ts b/packages/compiler/test/aot/emit_stubs_spec.ts new file mode 100644 index 0000000000000..3ec27c7eb5ee9 --- /dev/null +++ b/packages/compiler/test/aot/emit_stubs_spec.ts @@ -0,0 +1,69 @@ +/** + * @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 {MockDirectory, compile, expectNoDiagnostics, setup, toMockFileArray} from './test_util'; + +describe('aot stubs', () => { + let angularFiles = setup(); + + it('should create empty .ngfactory and .ngsummary files for every source file', () => { + const appDir = {'app.ts': `export const x = 1;`}; + const rootDir = {'app': appDir}; + const {genFiles} = + compile([rootDir, angularFiles], {postCompile: expectNoDiagnostics, stubsOnly: true}); + expect(genFiles.find((f) => f.genFileUrl === '/app/app.ngfactory.ts')).toBeTruthy(); + expect(genFiles.find((f) => f.genFileUrl === '/app/app.ngsummary.ts')).toBeTruthy(); + }); + + it('should create empty .ngstyle files for imported css files', () => { + const appDir = { + 'app.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + template: '', + styleUrls: ['./style.css'] + }) + export class MyComp {} + + @NgModule({ + declarations: [MyComp] + }) + export class MyModule {} + export const x = 1; + `, + 'style.css': '' + }; + const rootDir = {'app': appDir}; + const {genFiles} = + compile([rootDir, angularFiles], {postCompile: expectNoDiagnostics, stubsOnly: true}); + expect(genFiles.find((f) => f.genFileUrl === '/app/style.css.shim.ngstyle.ts')).toBeTruthy(); + }); + + it('should create stub exports for NgModules of the right type', () => { + const appDir = { + 'app.module.ts': ` + import { NgModule } from '@angular/core'; + + @NgModule() + export class MyModule {} + `, + 'app.boot.ts': ` + import {NgModuleFactory} from '@angular/core'; + import {MyModuleNgFactory} from './app.module.ngfactory'; + import {MyModuleNgSummary} from './app.module.ngsummary'; + import {MyModule} from './app.module'; + + export const factory: NgModuleFactory = MyModuleNgFactory; + export const summary: () => any[] = MyModuleNgSummary; + ` + }; + const rootDir = {'app': appDir}; + compile([rootDir, angularFiles], {postCompile: expectNoDiagnostics, stubsOnly: true}); + }); +}); diff --git a/packages/compiler/test/aot/test_util.ts b/packages/compiler/test/aot/test_util.ts index f81751fd7d960..db91f275f163f 100644 --- a/packages/compiler/test/aot/test_util.ts +++ b/packages/compiler/test/aot/test_util.ts @@ -234,15 +234,12 @@ export class MockCompilerHost implements ts.CompilerHost { } const effectiveName = this.getEffectiveName(fileName); if (effectiveName == fileName) { - let result = open(fileName, this.data) != null; - return result; - } else { - if (fileName.match(rxjs)) { - let result = fs.existsSync(effectiveName); - return result; - } - return false; + return open(fileName, this.data) != null; + } + if (fileName.match(rxjs)) { + return fs.existsSync(effectiveName); } + return false; } readFile(fileName: string): string { return this.getFileContent(fileName) !; } @@ -303,18 +300,13 @@ export class MockCompilerHost implements ts.CompilerHost { if (/^lib.*\.d\.ts$/.test(basename)) { let libPath = ts.getDefaultLibFilePath(settings); return fs.readFileSync(path.join(path.dirname(libPath), basename), 'utf8'); - } else { - let effectiveName = this.getEffectiveName(fileName); - if (effectiveName === fileName) { - const result = open(fileName, this.data); - return result; - } else { - if (fileName.match(rxjs)) { - if (fs.existsSync(fileName)) { - return fs.readFileSync(fileName, 'utf8'); - } - } - } + } + let effectiveName = this.getEffectiveName(fileName); + if (effectiveName === fileName) { + return open(fileName, this.data); + } + if (fileName.match(rxjs) && fs.existsSync(fileName)) { + return fs.readFileSync(fileName, 'utf8'); } } @@ -422,15 +414,14 @@ export class MockMetadataBundlerHost implements MetadataBundlerHost { function find(fileName: string, data: MockFileOrDirectory | undefined): MockFileOrDirectory| undefined { if (!data) return undefined; - let names = fileName.split('/'); + const names = fileName.split('/'); if (names.length && !names[0].length) names.shift(); let current: MockFileOrDirectory|undefined = data; - for (let name of names) { - if (typeof current === 'string') + for (const name of names) { + if (typeof current !== 'object') { return undefined; - else - current = (current)[name]; - if (!current) return undefined; + } + current = current[name]; } return current; } @@ -603,11 +594,12 @@ export function compile( useSummaries?: boolean, preCompile?: (program: ts.Program) => void, postCompile?: (program: ts.Program) => void, + stubsOnly?: boolean, }& AotCompilerOptions = {}, tsOptions: ts.CompilerOptions = {}): {genFiles: GeneratedFile[], outDir: MockDirectory} { // when using summaries, always emit so the next step can use the results. const emit = options.emit || options.useSummaries; - const preCompile = options.preCompile || expectNoDiagnostics; + const preCompile = options.preCompile || (() => {}); const postCompile = options.postCompile || expectNoDiagnostics; const rootDirArr = toMockFileArray(rootDirs); const scriptNames = rootDirArr.map(entry => entry.fileName).filter(isSource); @@ -620,9 +612,12 @@ export function compile( } const tsSettings = {...settings, ...tsOptions}; const program = ts.createProgram(host.scriptNames.slice(0), tsSettings, host); - if (preCompile) preCompile(program); + preCompile(program); const {compiler, reflector} = createAotCompiler(aotHost, options); - const genFiles = compiler.compileAllSync(program.getSourceFiles().map(sf => sf.fileName)); + const analyzedModules = + compiler.analyzeModulesSync(program.getSourceFiles().map(sf => sf.fileName)); + const genFiles = options.stubsOnly ? compiler.emitAllStubs(analyzedModules) : + compiler.emitAllImpls(analyzedModules); genFiles.forEach((file) => { const source = file.source || toTypeScript(file); if (isSource(file.genFileUrl)) { @@ -632,7 +627,7 @@ export function compile( } }); const newProgram = ts.createProgram(host.scriptNames.slice(0), tsSettings, host); - if (postCompile) postCompile(newProgram); + postCompile(newProgram); if (emit) { newProgram.emit(); }