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:
parent
ce8053103e
commit
31022cbecf
|
@ -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<string, FactoryInfo>();
|
||||
this.sourceToFactorySymbols = new Map<string, Set<string>>();
|
||||
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 =
|
||||
|
|
|
@ -10,3 +10,4 @@
|
|||
|
||||
export {FactoryGenerator, FactoryInfo, generatedFactoryTransform} from './src/factory_generator';
|
||||
export {GeneratedShimsHostWrapper} from './src/host';
|
||||
export {SummaryGenerator} from './src/summary_generator';
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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', `
|
||||
|
|
Loading…
Reference in New Issue