diff --git a/packages/bazel/src/ng_module.bzl b/packages/bazel/src/ng_module.bzl index 133ac0b3e4ff6..8b152c6f606ce 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 c3d0aea40b09b..35be8937d7d99 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 bd10daafb1944..4df350040a96f 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 7e349632d7058..35c8e36c204f9 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"],