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
This commit is contained in:
Alex Rickabaugh 2018-10-16 15:07:46 -07:00 committed by Alex Rickabaugh
parent ce8053103e
commit 31022cbecf
4 changed files with 100 additions and 4 deletions

View File

@ -17,7 +17,7 @@ import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecorato
import {BaseDefDecoratorHandler} from './annotations/src/base_def'; import {BaseDefDecoratorHandler} from './annotations/src/base_def';
import {TypeScriptReflectionHost} from './metadata'; import {TypeScriptReflectionHost} from './metadata';
import {FileResourceLoader, HostResourceLoader} from './resource_loader'; 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 {ivySwitchTransform} from './switch';
import {IvyCompilation, ivyTransformFactory} from './transform'; import {IvyCompilation, ivyTransformFactory} from './transform';
import {TypeCheckContext, TypeCheckProgramHost} from './typecheck'; import {TypeCheckContext, TypeCheckProgramHost} from './typecheck';
@ -56,12 +56,11 @@ export class NgtscProgram implements api.Program {
let rootFiles = [...rootNames]; let rootFiles = [...rootNames];
if (shouldGenerateShims) { if (shouldGenerateShims) {
// Summary generation. // Summary generation.
const summaryGenerator = SummaryGenerator.forRootFiles(rootNames);
// Factory generation. // Factory generation.
const factoryGenerator = FactoryGenerator.forRootFiles(rootNames); const factoryGenerator = FactoryGenerator.forRootFiles(rootNames);
const factoryFileMap = factoryGenerator.factoryFileMap; const factoryFileMap = factoryGenerator.factoryFileMap;
const factoryFileNames = Array.from(factoryFileMap.keys());
rootFiles.push(...factoryFileNames);
this.factoryToSourceInfo = new Map<string, FactoryInfo>(); this.factoryToSourceInfo = new Map<string, FactoryInfo>();
this.sourceToFactorySymbols = new Map<string, Set<string>>(); this.sourceToFactorySymbols = new Map<string, Set<string>>();
factoryFileMap.forEach((sourceFilePath, factoryPath) => { factoryFileMap.forEach((sourceFilePath, factoryPath) => {
@ -69,7 +68,10 @@ export class NgtscProgram implements api.Program {
this.sourceToFactorySymbols !.set(sourceFilePath, moduleSymbolNames); this.sourceToFactorySymbols !.set(sourceFilePath, moduleSymbolNames);
this.factoryToSourceInfo !.set(factoryPath, {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 = this.tsProgram =

View File

@ -10,3 +10,4 @@
export {FactoryGenerator, FactoryInfo, generatedFactoryTransform} from './src/factory_generator'; export {FactoryGenerator, FactoryInfo, generatedFactoryTransform} from './src/factory_generator';
export {GeneratedShimsHostWrapper} from './src/host'; export {GeneratedShimsHostWrapper} from './src/host';
export {SummaryGenerator} from './src/summary_generator';

View File

@ -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<string, string>) {}
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<string>): SummaryGenerator {
const map = new Map<string, string>();
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);
}

View File

@ -572,6 +572,38 @@ describe('ngtsc behavioral tests', () => {
expect(emptyFactory).toContain(`export var ɵNonEmptyModule = true;`); 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', () => { it('should compile a banana-in-a-box inside of a template', () => {
env.tsconfig(); env.tsconfig();
env.write('test.ts', ` env.write('test.ts', `