fix(compiler): mark NgModuleFactory construction as not side effectful (#38147)

This allows Closure compiler to tree shake unused constructor calls to `NgModuleFactory`, which is otherwise considered
side-effectful. The Angular compiler generates factory objects which are exported but typically not used, as they are
only needed for compatibility with View Engine. This results in top-level constructor calls, such as:

```typescript
export const FooNgFactory = new NgModuleFactory(Foo);
```

`NgModuleFactory` has a side-effecting constructor, so this statement cannot be tree shaken, even if `FooNgFactory` is
never imported. The `NgModuleFactory` continues to reference its associated `NgModule` and prevents the module and all
its unused dependencies from being tree shaken. This effectively prevents all components from being tree shaken, making
Closure builds significantly larger than they should be.

The fix here is to wrap `NgModuleFactory` constructor with `noSideEffects(() => /* ... */)`, which tricks the Closure
compiler into assuming that the invoked function has no side effects. This allows it to tree-shake unused
`NgModuleFactory()` constructors when they aren't imported. Since the factory can be removed, the module can also be
removed (if nothing else references it), thus tree shaking unused components as expected.

PR Close #38147
This commit is contained in:
Doug Parker 2020-07-17 16:23:21 -07:00 committed by Alex Rickabaugh
parent 887c350f9d
commit 7f8c2225f2
6 changed files with 18 additions and 7 deletions

View File

@ -65,6 +65,7 @@ const CORE_SUPPORTED_SYMBOLS = new Map<string, string>([
['ɵɵInjectorDef', 'ɵɵInjectorDef'],
['ɵɵNgModuleDefWithMeta', 'ɵɵNgModuleDefWithMeta'],
['ɵNgModuleFactory', 'NgModuleFactory'],
['ɵnoSideEffects', 'ɵnoSideEffects'],
]);
const CORE_MODULE = '@angular/core';

View File

@ -63,7 +63,8 @@ export class FactoryGenerator implements PerFileShimGenerator, FactoryTracker {
// because it won't miss any that do.
const varLines = symbolNames.map(
name => `export const ${
name}NgFactory: i0.ɵNgModuleFactory<any> = new i0.ɵNgModuleFactory(${name});`);
name}NgFactory: i0.ɵNgModuleFactory<any> = i0.ɵnoSideEffects(() => new i0.ɵNgModuleFactory(${
name}));`);
sourceText += [
// This might be incorrect if the current package being compiled is Angular core, but it's
// okay to leave in at type checking time. TypeScript can handle this reference via its path

View File

@ -103,3 +103,5 @@ export interface QueryList<T>/* implements Iterable<T> */ {
export type NgIterable<T> = Array<T>|Iterable<T>;
export class NgZone {}
export declare function ɵnoSideEffects<T>(fn: () => T): T;

View File

@ -3538,7 +3538,9 @@ runInEachFileSystem(os => {
expect(factoryContents).toContain(`import * as i0 from '@angular/core';`);
expect(factoryContents).toContain(`import { NotAModule, TestModule } from './test';`);
expect(factoryContents)
.toContain(`export var TestModuleNgFactory = new i0.\u0275NgModuleFactory(TestModule);`);
.toContain(
'export var TestModuleNgFactory = ' +
'i0.ɵnoSideEffects(function () { return new i0.\u0275NgModuleFactory(TestModule); });');
expect(factoryContents).not.toContain(`NotAModuleNgFactory`);
expect(factoryContents).not.toContain('\u0275NonEmptyModule');
@ -3677,11 +3679,14 @@ runInEachFileSystem(os => {
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);
`));
expect(normalize(factoryContents))
.toBe(
'import * as i0 from "./r3_symbols"; ' +
'import { TestModule } from \'./test\'; ' +
'export var TestModuleNgFactory = ' +
'i0.\u0275noSideEffects(function () { ' +
'return new i0.NgModuleFactory(TestModule); ' +
'});');
});
describe('file-level comments', () => {

View File

@ -292,5 +292,6 @@ export {
ɵɵsanitizeUrl,
ɵɵsanitizeUrlOrResourceUrl,
} from './sanitization/sanitization';
export {noSideEffects as ɵnoSideEffects} from './util/closure';
// clang-format on

View File

@ -28,6 +28,7 @@ export {ɵɵdefineNgModule} from './render3/definition';
export {ɵɵFactoryDef} from './render3/interfaces/definition';
export {setClassMetadata} from './render3/metadata';
export {NgModuleFactory} from './render3/ng_module_ref';
export {noSideEffects as ɵnoSideEffects} from './util/closure';