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,
"generateCodeForLibraries": False,
"allowEmptyCodegenFiles": True,
"generateNgFactoryShims": True,
"generateNgSummaryShims": True,
# Summaries are only enabled if Angular outputs are to be produced.
"enableSummariesForJit": is_legacy_ngc,
"enableIvy": _enable_ivy_value(ctx),

View File

@ -83,7 +83,15 @@ export class NgtscProgram implements api.Program {
this.rootDirs = getRootDirs(host, options);
this.closureCompilerEnabled = !!options.annotateForClosureCompiler;
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));
if (host.fileNameToModuleName !== undefined) {
this.fileToModuleHost = host as FileToModuleHost;
@ -91,10 +99,14 @@ export class NgtscProgram implements api.Program {
let rootFiles = [...rootNames];
const generators: ShimGenerator[] = [];
if (shouldGenerateShims) {
let summaryGenerator: SummaryGenerator|null = null;
if (shouldGenerateSummaryShims) {
// Summary generation.
const summaryGenerator = SummaryGenerator.forRootFiles(normalizedRootNames);
summaryGenerator = SummaryGenerator.forRootFiles(normalizedRootNames);
generators.push(summaryGenerator);
}
if (shouldGenerateFactoryShims) {
// Factory generation.
const factoryGenerator = FactoryGenerator.forRootFiles(normalizedRootNames);
const factoryFileMap = factoryGenerator.factoryFileMap;
@ -107,8 +119,14 @@ export class NgtscProgram implements api.Program {
});
const factoryFileNames = Array.from(factoryFileMap.keys());
rootFiles.push(...factoryFileNames, ...summaryGenerator.getSummaryFileNames());
generators.push(summaryGenerator, factoryGenerator);
rootFiles.push(...factoryFileNames);
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);

View File

@ -196,6 +196,23 @@ export interface CompilerOptions extends ts.CompilerOptions {
*/
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.
* This option defaults to `true`.

View File

@ -2340,7 +2340,7 @@ runInEachFileSystem(os => {
});
it('should generate correct factory stubs for a test module', () => {
env.tsconfig({'allowEmptyCodegenFiles': true});
env.tsconfig({'generateNgFactoryShims': true});
env.write('test.ts', `
import {Injectable, NgModule} from '@angular/core';
@ -2374,126 +2374,130 @@ runInEachFileSystem(os => {
expect(emptyFactory).toContain(`export var \u0275NonEmptyModule = true;`);
});
it('should generate correct type annotation for NgModuleFactory calls in ngfactories', () => {
env.tsconfig({'allowEmptyCodegenFiles': true});
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'test',
template: '...',
})
export class TestCmp {}
`);
env.driveMain();
describe('ngfactory shims', () => {
beforeEach(() => { env.tsconfig({'generateNgFactoryShims': true}); });
const ngfactoryContents = env.getContents('test.ngfactory.d.ts');
expect(ngfactoryContents).toContain(`i0.ɵNgModuleFactory<any>`);
it('should generate correct type annotation for NgModuleFactory calls in ngfactories', () => {
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'test',
template: '...',
})
export class TestCmp {}
`);
env.driveMain();
const ngfactoryContents = env.getContents('test.ngfactory.d.ts');
expect(ngfactoryContents).toContain(`i0.ɵNgModuleFactory<any>`);
});
it('should copy a top-level comment into a factory stub', () => {
env.tsconfig({'allowEmptyCodegenFiles': true});
env.write('test.ts', `/** I am a top-level comment. */
import {NgModule} from '@angular/core';
@NgModule({})
export class TestModule {}
`);
env.driveMain();
const factoryContents = env.getContents('test.ngfactory.js');
expect(factoryContents).toMatch(/^\/\*\* I am a top-level comment\. \*\//);
});
it('should be able to compile an app using the factory shim', () => {
env.tsconfig({'allowEmptyCodegenFiles': true});
env.write('test.ts', `
export {MyModuleNgFactory} from './my-module.ngfactory';
`);
env.write('my-module.ts', `
import {NgModule} from '@angular/core';
@NgModule({})
export class MyModule {}
`);
env.driveMain();
});
it('should generate correct imports in factory stubs when compiling @angular/core', () => {
env.tsconfig({'allowEmptyCodegenFiles': true});
env.write('test.ts', `
import {NgModule} from '@angular/core';
@NgModule({})
export class TestModule {}
`);
// Trick the compiler into thinking it's compiling @angular/core.
env.write('r3_symbols.ts', 'export const ITS_JUST_ANGULAR = true;');
env.driveMain();
const factoryContents = env.getContents('test.ngfactory.js');
expect(normalize(factoryContents)).toBe(normalize(`
import * as i0 from "./r3_symbols";
import { TestModule } from './test';
export var TestModuleNgFactory = new i0.NgModuleFactory(TestModule);
`));
});
});
it('should copy a top-level comment into a factory stub', () => {
env.tsconfig({'allowEmptyCodegenFiles': true});
env.write('test.ts', `/** I am a top-level comment. */
import {NgModule} from '@angular/core';
describe('ngsummary shim generation', () => {
beforeEach(() => { env.tsconfig({'generateNgSummaryShims': true}); });
@NgModule({})
export class TestModule {}
`);
env.driveMain();
it('should generate a summary stub for decorated classes in the input file only', () => {
env.write('test.ts', `
import {Injectable, NgModule} from '@angular/core';
export class NotAModule {}
@NgModule({})
export class TestModule {}
`);
const factoryContents = env.getContents('test.ngfactory.js');
expect(factoryContents).toMatch(/^\/\*\* I am a top-level comment\. \*\//);
env.driveMain();
const summaryContents = env.getContents('test.ngsummary.js');
expect(summaryContents).toEqual(`export var TestModuleNgSummary = null;\n`);
});
it('should generate a summary stub for classes exported via exports', () => {
env.write('test.ts', `
import {Injectable, NgModule} from '@angular/core';
@NgModule({})
class NotDirectlyExported {}
export {NotDirectlyExported};
`);
env.driveMain();
const summaryContents = env.getContents('test.ngsummary.js');
expect(summaryContents).toEqual(`export var NotDirectlyExportedNgSummary = 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.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 \u0275empty = null;\n`);
});
});
it('should be able to compile an app using the factory shim', () => {
env.tsconfig({'allowEmptyCodegenFiles': true});
env.write('test.ts', `
export {MyModuleNgFactory} from './my-module.ngfactory';
`);
env.write('my-module.ts', `
import {NgModule} from '@angular/core';
@NgModule({})
export class MyModule {}
`);
env.driveMain();
});
it('should generate correct imports in factory stubs when compiling @angular/core', () => {
env.tsconfig({'allowEmptyCodegenFiles': true});
env.write('test.ts', `
import {NgModule} from '@angular/core';
@NgModule({})
export class TestModule {}
`);
// Trick the compiler into thinking it's compiling @angular/core.
env.write('r3_symbols.ts', 'export const ITS_JUST_ANGULAR = true;');
env.driveMain();
const factoryContents = env.getContents('test.ngfactory.js');
expect(normalize(factoryContents)).toBe(normalize(`
import * as i0 from "./r3_symbols";
import { TestModule } from './test';
export var TestModuleNgFactory = new i0.NgModuleFactory(TestModule);
`));
});
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('should generate a summary stub for classes exported via exports', () => {
env.tsconfig({'allowEmptyCodegenFiles': true});
env.write('test.ts', `
import {Injectable, NgModule} from '@angular/core';
@NgModule({})
class NotDirectlyExported {}
export {NotDirectlyExported};
`);
env.driveMain();
const summaryContents = env.getContents('test.ngsummary.js');
expect(summaryContents).toEqual(`export var NotDirectlyExportedNgSummary = 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 \u0275empty = null;\n`);
});
it('should compile a banana-in-a-box inside of a template', () => {
env.write('test.ts', `
@ -2963,13 +2967,13 @@ runInEachFileSystem(os => {
it('should compile programs with typeRoots', () => {
// 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
// program automatically. Shims are also turned on (via allowEmptyCodegenFiles) because the
// shim ts.CompilerHost wrapper can break typeRoot functionality (which this test is meant to
// detect).
// program automatically. Shims are also turned on because the shim ts.CompilerHost wrapper
// can break typeRoot functionality (which this test is meant to detect).
env.write('tsconfig.json', `{
"extends": "./tsconfig-base.json",
"angularCompilerOptions": {
"allowEmptyCodegenFiles": true
"generateNgFactoryShims": true,
"generateNgSummaryShims": true,
},
"compilerOptions": {
"typeRoots": ["./testTypeRoot"],