From 31022cbecfcab0b45b2add444603a548ca7f72be Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Tue, 16 Oct 2018 15:07:46 -0700 Subject: [PATCH] feat(ivy): generate .ngsummary.js shims (#26495) This commit adds generation of .ngsummary.js shims alongside .ngfactory.js shims when generated files are enabled. Generated .ngsummary shims contain a single, null export for every exported class with decorators that exists in the original source files. Ivy code does not depend on summaries, so these exist only as a placeholder to allow them to be imported and their values passed to old APIs. This preserves backwards compatibility. Testing strategy: this commit adds a compiler test to verify the correct shape and contents of the generated .ngsummary.js files. PR Close #26495 --- packages/compiler-cli/src/ngtsc/program.ts | 10 +-- .../compiler-cli/src/ngtsc/shims/index.ts | 1 + .../src/ngtsc/shims/src/summary_generator.ts | 61 +++++++++++++++++++ .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 32 ++++++++++ 4 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/shims/src/summary_generator.ts 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', `