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
This commit is contained in:
parent
f7960c024c
commit
8e73f9b0aa
@ -223,9 +223,19 @@ function isEligibleForLowering(node: ts.Node | undefined): boolean {
|
|||||||
// Don't lower expressions in a declaration.
|
// Don't lower expressions in a declaration.
|
||||||
return false;
|
return false;
|
||||||
case ts.SyntaxKind.VariableDeclaration:
|
case ts.SyntaxKind.VariableDeclaration:
|
||||||
// Avoid lowering expressions already in an exported variable declaration
|
const isExported = (ts.getCombinedModifierFlags(node as ts.VariableDeclaration) &
|
||||||
return (ts.getCombinedModifierFlags(node as ts.VariableDeclaration) &
|
ts.ModifierFlags.Export) == 0;
|
||||||
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);
|
return isEligibleForLowering(node.parent);
|
||||||
}
|
}
|
||||||
|
@ -875,6 +875,42 @@ describe('ngc transformer command-line', () => {
|
|||||||
expect(mymoduleSource).toMatch(/ɵ0 = .*foo\(\)/);
|
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', () => {
|
it('should be able to lower supported expressions', () => {
|
||||||
writeConfig(`{
|
writeConfig(`{
|
||||||
"extends": "./tsconfig-base.json",
|
"extends": "./tsconfig-base.json",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user