feat(ivy): give shim generation its own compiler options (#33256)

As a hack to get the Ivy compiler ngtsc off the ground, the existing
'allowEmptyCodegenFiles' option was used to control generation of ngfactory
and ngsummary shims during compilation. This option was selected since it's
enabled in google3 but never enabled in external projects.

As ngtsc is now mature and the role shims play in compilation is now better
understood across the ecosystem, this commit introduces two new compiler
options to control shim generation:

* generateNgFactoryShims controls the generation of .ngfactory shims.
* generateNgSummaryShims controls the generation of .ngsummary shims.

The 'allowEmptyCodegenFiles' option is still honored if either of the above
flags are not set explicitly.

PR Close #33256
This commit is contained in:
Alex Rickabaugh 2019-10-18 12:15:25 -07:00 committed by Matias Niemelä
parent 29bc3a775f
commit d4db746898
4 changed files with 163 additions and 122 deletions

View File

@ -305,6 +305,8 @@ def _ngc_tsconfig(ctx, files, srcs, **kwargs):
"enableResourceInlining": ctx.attr.inline_resources, "enableResourceInlining": ctx.attr.inline_resources,
"generateCodeForLibraries": False, "generateCodeForLibraries": False,
"allowEmptyCodegenFiles": True, "allowEmptyCodegenFiles": True,
"generateNgFactoryShims": True,
"generateNgSummaryShims": True,
# Summaries are only enabled if Angular outputs are to be produced. # Summaries are only enabled if Angular outputs are to be produced.
"enableSummariesForJit": is_legacy_ngc, "enableSummariesForJit": is_legacy_ngc,
"enableIvy": _enable_ivy_value(ctx), "enableIvy": _enable_ivy_value(ctx),

View File

@ -83,7 +83,15 @@ export class NgtscProgram implements api.Program {
this.rootDirs = getRootDirs(host, options); this.rootDirs = getRootDirs(host, options);
this.closureCompilerEnabled = !!options.annotateForClosureCompiler; this.closureCompilerEnabled = !!options.annotateForClosureCompiler;
this.resourceManager = new HostResourceLoader(host, options); this.resourceManager = new HostResourceLoader(host, options);
const shouldGenerateShims = options.allowEmptyCodegenFiles || false; // TODO(alxhub): remove the fallback to allowEmptyCodegenFiles after verifying that the rest of
// our build tooling is no longer relying on it.
const allowEmptyCodegenFiles = options.allowEmptyCodegenFiles || false;
const shouldGenerateFactoryShims = options.generateNgFactoryShims !== undefined ?
options.generateNgFactoryShims :
allowEmptyCodegenFiles;
const shouldGenerateSummaryShims = options.generateNgSummaryShims !== undefined ?
options.generateNgSummaryShims :
allowEmptyCodegenFiles;
const normalizedRootNames = rootNames.map(n => absoluteFrom(n)); const normalizedRootNames = rootNames.map(n => absoluteFrom(n));
if (host.fileNameToModuleName !== undefined) { if (host.fileNameToModuleName !== undefined) {
this.fileToModuleHost = host as FileToModuleHost; this.fileToModuleHost = host as FileToModuleHost;
@ -91,10 +99,14 @@ export class NgtscProgram implements api.Program {
let rootFiles = [...rootNames]; let rootFiles = [...rootNames];
const generators: ShimGenerator[] = []; const generators: ShimGenerator[] = [];
if (shouldGenerateShims) { let summaryGenerator: SummaryGenerator|null = null;
if (shouldGenerateSummaryShims) {
// Summary generation. // Summary generation.
const summaryGenerator = SummaryGenerator.forRootFiles(normalizedRootNames); summaryGenerator = SummaryGenerator.forRootFiles(normalizedRootNames);
generators.push(summaryGenerator);
}
if (shouldGenerateFactoryShims) {
// Factory generation. // Factory generation.
const factoryGenerator = FactoryGenerator.forRootFiles(normalizedRootNames); const factoryGenerator = FactoryGenerator.forRootFiles(normalizedRootNames);
const factoryFileMap = factoryGenerator.factoryFileMap; const factoryFileMap = factoryGenerator.factoryFileMap;
@ -107,8 +119,14 @@ export class NgtscProgram implements api.Program {
}); });
const factoryFileNames = Array.from(factoryFileMap.keys()); const factoryFileNames = Array.from(factoryFileMap.keys());
rootFiles.push(...factoryFileNames, ...summaryGenerator.getSummaryFileNames()); rootFiles.push(...factoryFileNames);
generators.push(summaryGenerator, factoryGenerator); generators.push(factoryGenerator);
}
// Done separately to preserve the order of factory files before summary files in rootFiles.
// TODO(alxhub): validate that this is necessary.
if (shouldGenerateSummaryShims) {
rootFiles.push(...summaryGenerator !.getSummaryFileNames());
} }
this.typeCheckFilePath = typeCheckFilePath(this.rootDirs); this.typeCheckFilePath = typeCheckFilePath(this.rootDirs);

View File

@ -196,6 +196,23 @@ export interface CompilerOptions extends ts.CompilerOptions {
*/ */
enableResourceInlining?: boolean; enableResourceInlining?: boolean;
/**
* Controls whether ngtsc will emit `.ngfactory.js` shims for each compiled `.ts` file.
*
* These shims support legacy imports from `ngfactory` files, by exporting a factory shim
* for each component or NgModule in the original `.ts` file.
*/
generateNgFactoryShims?: boolean;
/**
* Controls whether ngtsc will emit `.ngsummary.js` shims for each compiled `.ts` file.
*
* These shims support legacy imports from `ngsummary` files, by exporting an empty object
* for each NgModule in the original `.ts` file. The only purpose of summaries is to feed them to
* `TestBed`, which is a no-op in Ivy.
*/
generateNgSummaryShims?: boolean;
/** /**
* Tells the compiler to generate definitions using the Render3 style code generation. * Tells the compiler to generate definitions using the Render3 style code generation.
* This option defaults to `true`. * This option defaults to `true`.

View File

@ -2340,7 +2340,7 @@ runInEachFileSystem(os => {
}); });
it('should generate correct factory stubs for a test module', () => { it('should generate correct factory stubs for a test module', () => {
env.tsconfig({'allowEmptyCodegenFiles': true}); env.tsconfig({'generateNgFactoryShims': true});
env.write('test.ts', ` env.write('test.ts', `
import {Injectable, NgModule} from '@angular/core'; import {Injectable, NgModule} from '@angular/core';
@ -2374,8 +2374,10 @@ runInEachFileSystem(os => {
expect(emptyFactory).toContain(`export var \u0275NonEmptyModule = true;`); expect(emptyFactory).toContain(`export var \u0275NonEmptyModule = true;`);
}); });
describe('ngfactory shims', () => {
beforeEach(() => { env.tsconfig({'generateNgFactoryShims': true}); });
it('should generate correct type annotation for NgModuleFactory calls in ngfactories', () => { it('should generate correct type annotation for NgModuleFactory calls in ngfactories', () => {
env.tsconfig({'allowEmptyCodegenFiles': true});
env.write('test.ts', ` env.write('test.ts', `
import {Component} from '@angular/core'; import {Component} from '@angular/core';
@Component({ @Component({
@ -2444,10 +2446,13 @@ runInEachFileSystem(os => {
export var TestModuleNgFactory = new i0.NgModuleFactory(TestModule); export var TestModuleNgFactory = new i0.NgModuleFactory(TestModule);
`)); `));
}); });
});
describe('ngsummary shim generation', () => {
beforeEach(() => { env.tsconfig({'generateNgSummaryShims': true}); });
it('should generate a summary stub for decorated classes in the input file only', () => { it('should generate a summary stub for decorated classes in the input file only', () => {
env.tsconfig({'allowEmptyCodegenFiles': true});
env.write('test.ts', ` env.write('test.ts', `
import {Injectable, NgModule} from '@angular/core'; import {Injectable, NgModule} from '@angular/core';
@ -2464,8 +2469,6 @@ runInEachFileSystem(os => {
}); });
it('should generate a summary stub for classes exported via exports', () => { it('should generate a summary stub for classes exported via exports', () => {
env.tsconfig({'allowEmptyCodegenFiles': true});
env.write('test.ts', ` env.write('test.ts', `
import {Injectable, NgModule} from '@angular/core'; import {Injectable, NgModule} from '@angular/core';
@ -2483,7 +2486,6 @@ runInEachFileSystem(os => {
it('it should generate empty export when there are no other summary symbols, to ensure the output is a valid ES module', 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', ` env.write('empty.ts', `
export class NotAModule {} export class NotAModule {}
`); `);
@ -2494,6 +2496,8 @@ runInEachFileSystem(os => {
// The empty export ensures this js file is still an ES module. // The empty export ensures this js file is still an ES module.
expect(emptySummary).toEqual(`export var \u0275empty = null;\n`); expect(emptySummary).toEqual(`export var \u0275empty = 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.write('test.ts', ` env.write('test.ts', `
@ -2963,13 +2967,13 @@ runInEachFileSystem(os => {
it('should compile programs with typeRoots', () => { it('should compile programs with typeRoots', () => {
// Write out a custom tsconfig.json that includes 'typeRoots' and 'files'. 'files' is // Write out a custom tsconfig.json that includes 'typeRoots' and 'files'. 'files' is
// necessary because otherwise TS picks up the testTypeRoot/test/index.d.ts file into the // necessary because otherwise TS picks up the testTypeRoot/test/index.d.ts file into the
// program automatically. Shims are also turned on (via allowEmptyCodegenFiles) because the // program automatically. Shims are also turned on because the shim ts.CompilerHost wrapper
// shim ts.CompilerHost wrapper can break typeRoot functionality (which this test is meant to // can break typeRoot functionality (which this test is meant to detect).
// detect).
env.write('tsconfig.json', `{ env.write('tsconfig.json', `{
"extends": "./tsconfig-base.json", "extends": "./tsconfig-base.json",
"angularCompilerOptions": { "angularCompilerOptions": {
"allowEmptyCodegenFiles": true "generateNgFactoryShims": true,
"generateNgSummaryShims": true,
}, },
"compilerOptions": { "compilerOptions": {
"typeRoots": ["./testTypeRoot"], "typeRoots": ["./testTypeRoot"],