diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 12cc5684b5..c925718dca 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -17,7 +17,7 @@ import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecorato import {BaseDefDecoratorHandler} from './annotations/src/base_def'; import {TypeScriptReflectionHost} from './metadata'; import {FileResourceLoader, HostResourceLoader} from './resource_loader'; -import {FactoryGenerator, FactoryInfo, GeneratedShimsHostWrapper, generatedFactoryTransform} from './shims'; +import {FactoryGenerator, FactoryInfo, GeneratedShimsHostWrapper, SummaryGenerator, generatedFactoryTransform} from './shims'; import {ivySwitchTransform} from './switch'; import {IvyCompilation, ivyTransformFactory} from './transform'; import {TypeCheckContext, TypeCheckProgramHost} from './typecheck'; @@ -56,12 +56,11 @@ export class NgtscProgram implements api.Program { let rootFiles = [...rootNames]; if (shouldGenerateShims) { // Summary generation. + const summaryGenerator = SummaryGenerator.forRootFiles(rootNames); // Factory generation. const factoryGenerator = FactoryGenerator.forRootFiles(rootNames); const factoryFileMap = factoryGenerator.factoryFileMap; - const factoryFileNames = Array.from(factoryFileMap.keys()); - rootFiles.push(...factoryFileNames); this.factoryToSourceInfo = new Map(); this.sourceToFactorySymbols = new Map>(); factoryFileMap.forEach((sourceFilePath, factoryPath) => { @@ -69,7 +68,10 @@ export class NgtscProgram implements api.Program { this.sourceToFactorySymbols !.set(sourceFilePath, moduleSymbolNames); this.factoryToSourceInfo !.set(factoryPath, {sourceFilePath, moduleSymbolNames}); }); - this.host = new GeneratedShimsHostWrapper(host, [factoryGenerator]); + + const factoryFileNames = Array.from(factoryFileMap.keys()); + rootFiles.push(...factoryFileNames, ...summaryGenerator.getSummaryFileNames()); + this.host = new GeneratedShimsHostWrapper(host, [summaryGenerator, factoryGenerator]); } this.tsProgram = diff --git a/packages/compiler-cli/src/ngtsc/shims/index.ts b/packages/compiler-cli/src/ngtsc/shims/index.ts index 5323905db3..3980a4362c 100644 --- a/packages/compiler-cli/src/ngtsc/shims/index.ts +++ b/packages/compiler-cli/src/ngtsc/shims/index.ts @@ -10,3 +10,4 @@ export {FactoryGenerator, FactoryInfo, generatedFactoryTransform} from './src/factory_generator'; export {GeneratedShimsHostWrapper} from './src/host'; +export {SummaryGenerator} from './src/summary_generator'; diff --git a/packages/compiler-cli/src/ngtsc/shims/src/summary_generator.ts b/packages/compiler-cli/src/ngtsc/shims/src/summary_generator.ts new file mode 100644 index 0000000000..799c3187c6 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/shims/src/summary_generator.ts @@ -0,0 +1,61 @@ +/** + * @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 * as ts from 'typescript'; + +import {ShimGenerator} from './host'; +import {isNonDeclarationTsFile} from './util'; + +export class SummaryGenerator implements ShimGenerator { + private constructor(private map: Map) {} + + getSummaryFileNames(): string[] { return Array.from(this.map.keys()); } + + getOriginalSourceOfShim(fileName: string): string|null { return this.map.get(fileName) || null; } + + generate(original: ts.SourceFile, genFilePath: string): ts.SourceFile { + // Collect a list of classes that need to have factory types emitted for them. This list is + // overly broad as at this point the ts.TypeChecker has not been created and so it can't be used + // to semantically understand which decorators are Angular decorators. It's okay to output an + // overly broad set of summary exports as the exports are no-ops anyway, and summaries are a + // compatibility layer which will be removed after Ivy is enabled. + const symbolNames = original + .statements + // Pick out top level class declarations... + .filter(ts.isClassDeclaration) + // which are named, exported, and have decorators. + .filter( + decl => isExported(decl) && decl.decorators !== undefined && + decl.name !== undefined) + // Grab the symbol name. + .map(decl => decl.name !.text); + + const varLines = symbolNames.map(name => `export const ${name}NgSummary: any = null;`); + + if (varLines.length === 0) { + // In the event there are no other exports, add an empty export to ensure the generated + // summary file is still an ES module. + varLines.push(`export const ɵempty = null;`); + } + const sourceText = varLines.join('\n'); + return ts.createSourceFile( + genFilePath, sourceText, original.languageVersion, true, ts.ScriptKind.TS); + } + + static forRootFiles(files: ReadonlyArray): SummaryGenerator { + const map = new Map(); + files.filter(sourceFile => isNonDeclarationTsFile(sourceFile)) + .forEach(sourceFile => map.set(sourceFile.replace(/\.ts$/, '.ngsummary.ts'), sourceFile)); + return new SummaryGenerator(map); + } +} + +function isExported(decl: ts.Declaration): boolean { + return decl.modifiers !== undefined && + decl.modifiers.some(mod => mod.kind == ts.SyntaxKind.ExportKeyword); +} diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 58d43230c9..7f9244b1f0 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -572,6 +572,38 @@ describe('ngtsc behavioral tests', () => { expect(emptyFactory).toContain(`export var ɵNonEmptyModule = true;`); }); + it('should generate a summary stub for decorated classes in the input file only', () => { + env.tsconfig({'allowEmptyCodegenFiles': true}); + + env.write('test.ts', ` + import {Injectable, NgModule} from '@angular/core'; + + export class NotAModule {} + + @NgModule({}) + export class TestModule {} + `); + + env.driveMain(); + + const summaryContents = env.getContents('test.ngsummary.js'); + expect(summaryContents).toEqual(`export var TestModuleNgSummary = null;\n`); + }); + + it('it should generate empty export when there are no other summary symbols, to ensure the output is a valid ES module', + () => { + env.tsconfig({'allowEmptyCodegenFiles': true}); + env.write('empty.ts', ` + export class NotAModule {} + `); + + env.driveMain(); + + const emptySummary = env.getContents('empty.ngsummary.js'); + // The empty export ensures this js file is still an ES module. + expect(emptySummary).toEqual(`export var ɵempty = null;\n`); + }); + it('should compile a banana-in-a-box inside of a template', () => { env.tsconfig(); env.write('test.ts', `