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 {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 =
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;`);
|
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', `
|
||||||
|
|
Loading…
Reference in New Issue