From efb99f1a828700dc1061e8fbd63c4cb45db05ab5 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Fri, 18 Oct 2019 12:15:25 -0700 Subject: [PATCH] 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 --- packages/bazel/src/ng_module.bzl | 2 + packages/compiler-cli/src/ngtsc/program.ts | 28 ++- packages/compiler-cli/src/transformers/api.ts | 17 ++ .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 198 +++++++++--------- 4 files changed, 143 insertions(+), 102 deletions(-) diff --git a/packages/bazel/src/ng_module.bzl b/packages/bazel/src/ng_module.bzl index 133ac0b3e4ff63..8b152c6f606cec 100644 --- a/packages/bazel/src/ng_module.bzl +++ b/packages/bazel/src/ng_module.bzl @@ -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), diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index c3d0aea40b09be..35be8937d7d995 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -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); diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index bd10daafb19445..4df350040a96fc 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -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`. diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 7e349632d70584..35c8e36c204f93 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -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(); - - const ngfactoryContents = env.getContents('test.ngfactory.d.ts'); - expect(ngfactoryContents).toContain(`i0.ɵNgModuleFactory`); - }); - - it('should copy a top-level comment into a factory stub', () => { - env.tsconfig({'allowEmptyCodegenFiles': true}); + describe('ngfactory shims', () => { + beforeEach(() => { env.tsconfig({'generateNgFactoryShims': true}); }); - env.write('test.ts', `/** I am a top-level comment. */ - import {NgModule} from '@angular/core'; + 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(); - @NgModule({}) - export class TestModule {} - `); - env.driveMain(); + const ngfactoryContents = env.getContents('test.ngfactory.d.ts'); + expect(ngfactoryContents).toContain(`i0.ɵNgModuleFactory`); + }); - const factoryContents = env.getContents('test.ngfactory.js'); - expect(factoryContents).toMatch(/^\/\*\* I am a top-level comment\. \*\//); - }); + it('should copy a top-level comment into a factory stub', () => { + env.tsconfig({'allowEmptyCodegenFiles': true}); - it('should be able to compile an app using the factory shim', () => { - 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(); - env.write('test.ts', ` - export {MyModuleNgFactory} from './my-module.ngfactory'; - `); + const factoryContents = env.getContents('test.ngfactory.js'); + expect(factoryContents).toMatch(/^\/\*\* I am a top-level comment\. \*\//); + }); - env.write('my-module.ts', ` - import {NgModule} from '@angular/core'; + it('should be able to compile an app using the factory shim', () => { + env.tsconfig({'allowEmptyCodegenFiles': true}); - @NgModule({}) - export class MyModule {} - `); + env.write('test.ts', ` + export {MyModuleNgFactory} from './my-module.ngfactory'; + `); - env.driveMain(); - }); + env.write('my-module.ts', ` + import {NgModule} from '@angular/core'; + + @NgModule({}) + export class MyModule {} + `); - it('should generate correct imports in factory stubs when compiling @angular/core', () => { - env.tsconfig({'allowEmptyCodegenFiles': true}); + env.driveMain(); + }); - env.write('test.ts', ` - import {NgModule} from '@angular/core'; + it('should generate correct imports in factory stubs when compiling @angular/core', () => { + env.tsconfig({'allowEmptyCodegenFiles': true}); - @NgModule({}) - export class TestModule {} - `); + 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;'); + // Trick the compiler into thinking it's compiling @angular/core. + env.write('r3_symbols.ts', 'export const ITS_JUST_ANGULAR = true;'); - env.driveMain(); + 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); - `)); + 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 {} + describe('ngsummary shim generation', () => { + beforeEach(() => { env.tsconfig({'generateNgSummaryShims': true}); }); - @NgModule({}) - export class TestModule {} - `); + 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 {} + `); - env.driveMain(); + env.driveMain(); - const summaryContents = env.getContents('test.ngsummary.js'); - expect(summaryContents).toEqual(`export var TestModuleNgSummary = null;\n`); - }); + 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}); + 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.write('test.ts', ` - import {Injectable, NgModule} from '@angular/core'; + env.driveMain(); - @NgModule({}) - class NotDirectlyExported {} + const summaryContents = env.getContents('test.ngsummary.js'); + expect(summaryContents).toEqual(`export var NotDirectlyExportedNgSummary = null;\n`); + }); - export {NotDirectlyExported}; - `); + 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(); + env.driveMain(); - const summaryContents = env.getContents('test.ngsummary.js'); - expect(summaryContents).toEqual(`export var NotDirectlyExportedNgSummary = null;\n`); + 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('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"],