Skip to content

Commit

Permalink
feat(compiler-cli): lower some exported expressions (#30038)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
alxhub authored and benlesh committed Apr 23, 2019
1 parent f7960c0 commit 8e73f9b
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 3 deletions.
16 changes: 13 additions & 3 deletions packages/compiler-cli/src/transformers/lower_expressions.ts
Expand Up @@ -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);
}
Expand Down
36 changes: 36 additions & 0 deletions packages/compiler-cli/test/ngc_spec.ts
Expand Up @@ -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",
Expand Down

0 comments on commit 8e73f9b

Please sign in to comment.