fix(ivy): in ngcc, handle inline exports in commonjs code (#32129)
One of the compiler's tasks is to enumerate the exports of a given ES module. This can happen for example to resolve `foo.bar` where `foo` is a namespace import: ```typescript import * as foo from './foo'; @NgModule({ directives: [foo.DIRECTIVES], }) ``` In this case, the compiler must enumerate the exports of `foo.ts` in order to evaluate the expression `foo.DIRECTIVES`. When this operation occurs under ngcc, it must deal with the different module formats and types of exports that occur. In commonjs code, a problem arises when certain exports are downleveled. ```typescript export const DIRECTIVES = [ FooDir, BarDir, ]; ``` can be downleveled to: ```javascript exports.DIRECTIVES = [ FooDir, BarDir, ``` Previously, ngtsc and ngcc expected that any export would have an associated `ts.Declaration` node. `export class`, `export function`, etc. all retain `ts.Declaration`s even when downleveled. But the `export const` construct above does not. Therefore, ngcc would not detect `DIRECTIVES` as an export of `foo.ts`, and the evaluation of `foo.DIRECTIVES` would therefore fail. To solve this problem, the core concept of an exported `Declaration` according to the `ReflectionHost` API is split into a `ConcreteDeclaration` which has a `ts.Declaration`, and an `InlineDeclaration` which instead has a `ts.Expression`. Differentiating between these allows ngcc to return an `InlineDeclaration` for `DIRECTIVES` and correctly keep track of this export. PR Close #32129
This commit is contained in:

committed by
Andrew Kushnir

parent
69ce1c2d41
commit
02bab8cf90
@ -9,7 +9,7 @@ import * as ts from 'typescript';
|
||||
|
||||
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
|
||||
import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||
import {ClassMemberKind, CtorParameter, Import, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection';
|
||||
import {ClassMemberKind, CtorParameter, Import, InlineDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection';
|
||||
import {getDeclaration} from '../../../src/ngtsc/testing';
|
||||
import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
|
||||
import {CommonJsReflectionHost} from '../../src/host/commonjs_host';
|
||||
@ -31,6 +31,7 @@ runInEachFileSystem(() => {
|
||||
let SIMPLE_ES2015_CLASS_FILE: TestFile;
|
||||
let SIMPLE_CLASS_FILE: TestFile;
|
||||
let FOO_FUNCTION_FILE: TestFile;
|
||||
let INLINE_EXPORT_FILE: TestFile;
|
||||
let INVALID_DECORATORS_FILE: TestFile;
|
||||
let INVALID_DECORATOR_ARGS_FILE: TestFile;
|
||||
let INVALID_PROP_DECORATORS_FILE: TestFile;
|
||||
@ -164,6 +165,18 @@ exports.foo = foo;
|
||||
`,
|
||||
};
|
||||
|
||||
INLINE_EXPORT_FILE = {
|
||||
name: _('/inline_export.js'),
|
||||
contents: `
|
||||
var core = require('@angular/core');
|
||||
function foo() {}
|
||||
foo.decorators = [
|
||||
{ type: core.Directive, args: [{ selector: '[ignored]' },] }
|
||||
];
|
||||
exports.directives = [foo];
|
||||
`,
|
||||
};
|
||||
|
||||
INVALID_DECORATORS_FILE = {
|
||||
name: _('/invalid_decorators.js'),
|
||||
contents: `
|
||||
@ -1629,7 +1642,7 @@ exports.ExternalModule = ExternalModule;
|
||||
const exportDeclarations = host.getExportsOfModule(file);
|
||||
expect(exportDeclarations).not.toBe(null);
|
||||
expect(Array.from(exportDeclarations !.entries())
|
||||
.map(entry => [entry[0], entry[1].node.getText(), entry[1].viaModule]))
|
||||
.map(entry => [entry[0], entry[1].node !.getText(), entry[1].viaModule]))
|
||||
.toEqual([
|
||||
['Directive', `Directive: FnWithArg<(clazz: any) => any>`, '@angular/core'],
|
||||
['a', `a = 'a'`, './a_module'],
|
||||
@ -1655,7 +1668,7 @@ exports.ExternalModule = ExternalModule;
|
||||
const exportDeclarations = host.getExportsOfModule(file);
|
||||
expect(exportDeclarations).not.toBe(null);
|
||||
expect(Array.from(exportDeclarations !.entries())
|
||||
.map(entry => [entry[0], entry[1].node.getText(), entry[1].viaModule]))
|
||||
.map(entry => [entry[0], entry[1].node !.getText(), entry[1].viaModule]))
|
||||
.toEqual([
|
||||
['Directive', `Directive: FnWithArg<(clazz: any) => any>`, _('/b_module')],
|
||||
['a', `a = 'a'`, _('/b_module')],
|
||||
@ -1673,6 +1686,19 @@ exports.ExternalModule = ExternalModule;
|
||||
['xtra2', `xtra2 = 'xtra2'`, _('/xtra_module')],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle inline exports', () => {
|
||||
loadTestFiles([INLINE_EXPORT_FILE]);
|
||||
const {program, host: compilerHost} = makeTestBundleProgram(_('/inline_export.js'));
|
||||
const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
|
||||
const file = getSourceFileOrError(program, _('/inline_export.js'));
|
||||
const exportDeclarations = host.getExportsOfModule(file);
|
||||
expect(exportDeclarations).not.toBeNull();
|
||||
const decl = exportDeclarations !.get('directives') as InlineDeclaration;
|
||||
expect(decl).not.toBeUndefined();
|
||||
expect(decl.node).toBeNull();
|
||||
expect(decl.expression).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getClassSymbol()', () => {
|
||||
|
@ -1538,8 +1538,9 @@ runInEachFileSystem(() => {
|
||||
'SomeClass',
|
||||
]);
|
||||
|
||||
const values = Array.from(exportDeclarations !.values())
|
||||
.map(declaration => [declaration.node.getText(), declaration.viaModule]);
|
||||
const values =
|
||||
Array.from(exportDeclarations !.values())
|
||||
.map(declaration => [declaration.node !.getText(), declaration.viaModule]);
|
||||
expect(values).toEqual([
|
||||
[`Directive: FnWithArg<(clazz: any) => any>`, null],
|
||||
[`a = 'a'`, null],
|
||||
|
@ -399,7 +399,7 @@ export { SomeDirective };
|
||||
|
||||
const declaration = host.getDeclarationOfIdentifier(ngModuleRef !);
|
||||
expect(declaration).not.toBe(null);
|
||||
expect(declaration !.node.getText()).toContain('function HttpClientXsrfModule()');
|
||||
expect(declaration !.node !.getText()).toContain('function HttpClientXsrfModule()');
|
||||
});
|
||||
});
|
||||
describe('getVariableValue', () => {
|
||||
|
@ -1842,8 +1842,9 @@ runInEachFileSystem(() => {
|
||||
'SomeClass',
|
||||
]);
|
||||
|
||||
const values = Array.from(exportDeclarations !.values())
|
||||
.map(declaration => [declaration.node.getText(), declaration.viaModule]);
|
||||
const values =
|
||||
Array.from(exportDeclarations !.values())
|
||||
.map(declaration => [declaration.node !.getText(), declaration.viaModule]);
|
||||
expect(values).toEqual([
|
||||
[`Directive: FnWithArg<(clazz: any) => any>`, null],
|
||||
[`a = 'a'`, null],
|
||||
|
@ -1741,7 +1741,7 @@ runInEachFileSystem(() => {
|
||||
const exportDeclarations = host.getExportsOfModule(file);
|
||||
expect(exportDeclarations).not.toBe(null);
|
||||
expect(Array.from(exportDeclarations !.entries())
|
||||
.map(entry => [entry[0], entry[1].node.getText(), entry[1].viaModule]))
|
||||
.map(entry => [entry[0], entry[1].node !.getText(), entry[1].viaModule]))
|
||||
.toEqual([
|
||||
['Directive', `Directive: FnWithArg<(clazz: any) => any>`, '@angular/core'],
|
||||
['a', `a = 'a'`, '/a_module'],
|
||||
|
Reference in New Issue
Block a user