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:
Alex Rickabaugh
2019-08-13 16:08:53 -07:00
committed by Andrew Kushnir
parent 69ce1c2d41
commit 02bab8cf90
17 changed files with 157 additions and 59 deletions

View File

@ -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()', () => {

View File

@ -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],

View File

@ -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', () => {

View File

@ -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],

View File

@ -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'],