From 8e73f9b0aaaa64f6321f3fa5cbeb3aa7c52b352e Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Mon, 22 Apr 2019 15:22:52 -0700 Subject: [PATCH] feat(compiler-cli): lower some exported expressions (#30038) The compiler uses metadata to represent what it statically knows about various expressions in a program. Occasionally, expressions in the program for which metadata is extracted may contain sub-expressions which are not representable in metadata. One such construct is an arrow function. The compiler does not always need to understand such expressions completely. For example, for a provider defined with `useValue`, the compiler does not need to understand the value at all, only the outer provider definition. In this case, the compiler employs a technique known as "expression lowering", where it rewrites the provider expression into one that can be represented in metadata. Chiefly, this involves extracting out the dynamic part (the `useValue` expression) into an exported constant. Lowering is applied through a heuristic, which considers the containing statement as well as the field name of the expression. Previously, this heuristic was not completely accurate in the case of route definitions and the `loadChildren` field, which is lowered. If the route definition using `loadChildren` existed inside a decorator invocation, lowering was performed correctly. However, if it existed inside a standalone variable declaration with an export keyword, the heuristic would conclude that lowering was unnecessary. For ordinary providers this is true; however the compiler attempts to fully understand the ROUTES token and thus even if an array of routes is declared in an exported variable, any `loadChildren` expressions within still need to be lowered. This commit enables lowering of already exported variables under a limited set of conditions (where the initializer expression is of a specific form). This should enable the use of `loadChildren` in route definitions. PR Close #30038 --- .../src/transformers/lower_expressions.ts | 16 +++++++-- packages/compiler-cli/test/ngc_spec.ts | 36 +++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/compiler-cli/src/transformers/lower_expressions.ts b/packages/compiler-cli/src/transformers/lower_expressions.ts index 609a8bcb73..3a39e7b2e9 100644 --- a/packages/compiler-cli/src/transformers/lower_expressions.ts +++ b/packages/compiler-cli/src/transformers/lower_expressions.ts @@ -223,9 +223,19 @@ function isEligibleForLowering(node: ts.Node | undefined): boolean { // Don't lower expressions in a declaration. return false; case ts.SyntaxKind.VariableDeclaration: - // Avoid lowering expressions already in an exported variable declaration - return (ts.getCombinedModifierFlags(node as ts.VariableDeclaration) & - ts.ModifierFlags.Export) == 0; + const isExported = (ts.getCombinedModifierFlags(node as ts.VariableDeclaration) & + ts.ModifierFlags.Export) == 0; + // This might be unnecessary, as the variable might be exported and only used as a reference + // in another expression. However, the variable also might be involved in provider + // definitions. If that's the case, there is a specific token (`ROUTES`) which the compiler + // attempts to understand deeply. Sub-expressions within that token (`loadChildren` for + // example) might also require lowering even if the top-level declaration is already + // properly exported. + const varNode = node as ts.VariableDeclaration; + return isExported || (varNode.initializer !== undefined && + (ts.isObjectLiteralExpression(varNode.initializer) || + ts.isArrayLiteralExpression(varNode.initializer) || + ts.isCallExpression(varNode.initializer))); } return isEligibleForLowering(node.parent); } diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index f6b79f9e53..cc2258882a 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -875,6 +875,42 @@ describe('ngc transformer command-line', () => { expect(mymoduleSource).toMatch(/ɵ0 = .*foo\(\)/); }); + it('should lower loadChildren in an exported variable expression', () => { + write('mymodule.ts', ` + import {Component, NgModule} from '@angular/core'; + import {RouterModule} from '@angular/router'; + + export function foo(): string { + console.log('side-effect'); + return 'test'; + } + + @Component({ + selector: 'route', + template: 'route', + }) + export class Route {} + + export const routes = [ + {path: '', pathMatch: 'full', component: Route, loadChildren: foo()} + ]; + + @NgModule({ + declarations: [Route], + imports: [ + RouterModule.forRoot(routes), + ] + }) + export class MyModule {} + `); + expect(compile()).toEqual(0); + + const mymodulejs = path.resolve(outDir, 'mymodule.js'); + const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8'); + expect(mymoduleSource).toContain('loadChildren: ɵ0'); + expect(mymoduleSource).toMatch(/ɵ0 = .*foo\(\)/); + }); + it('should be able to lower supported expressions', () => { writeConfig(`{ "extends": "./tsconfig-base.json",