2018-12-07 13:10:52 +00:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
|
|
|
*
|
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
|
*/
|
|
|
|
|
import * as ts from 'typescript';
|
|
|
|
|
|
2019-03-20 13:47:58 +00:00
|
|
|
import {ReferencesRegistry} from '../../../src/ngtsc/annotations';
|
|
|
|
|
import {Reference} from '../../../src/ngtsc/imports';
|
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
2019-08-13 16:08:53 -07:00
|
|
|
import {ClassDeclaration, ConcreteDeclaration} from '../../../src/ngtsc/reflection';
|
2019-03-20 13:47:58 +00:00
|
|
|
import {ModuleWithProvidersFunction, NgccReflectionHost} from '../host/ngcc_host';
|
2019-03-05 23:29:28 +01:00
|
|
|
import {hasNameIdentifier, isDefined} from '../utils';
|
2018-12-07 13:10:52 +00:00
|
|
|
|
|
|
|
|
export interface ModuleWithProvidersInfo {
|
|
|
|
|
/**
|
|
|
|
|
* The declaration (in the .d.ts file) of the function that returns
|
|
|
|
|
* a `ModuleWithProviders object, but has a signature that needs
|
|
|
|
|
* a type parameter adding.
|
|
|
|
|
*/
|
|
|
|
|
declaration: ts.MethodDeclaration|ts.FunctionDeclaration;
|
|
|
|
|
/**
|
|
|
|
|
* The NgModule class declaration (in the .d.ts file) to add as a type parameter.
|
|
|
|
|
*/
|
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
2019-08-13 16:08:53 -07:00
|
|
|
ngModule: ConcreteDeclaration<ClassDeclaration>;
|
2018-12-07 13:10:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ModuleWithProvidersAnalyses = Map<ts.SourceFile, ModuleWithProvidersInfo[]>;
|
|
|
|
|
export const ModuleWithProvidersAnalyses = Map;
|
|
|
|
|
|
|
|
|
|
export class ModuleWithProvidersAnalyzer {
|
2019-11-16 21:12:58 +01:00
|
|
|
constructor(
|
|
|
|
|
private host: NgccReflectionHost, private referencesRegistry: ReferencesRegistry,
|
|
|
|
|
private processDts: boolean) {}
|
2018-12-07 13:10:52 +00:00
|
|
|
|
|
|
|
|
analyzeProgram(program: ts.Program): ModuleWithProvidersAnalyses {
|
|
|
|
|
const analyses = new ModuleWithProvidersAnalyses();
|
|
|
|
|
const rootFiles = this.getRootFiles(program);
|
|
|
|
|
rootFiles.forEach(f => {
|
|
|
|
|
const fns = this.host.getModuleWithProvidersFunctions(f);
|
|
|
|
|
fns && fns.forEach(fn => {
|
2019-09-29 23:26:41 +02:00
|
|
|
if (fn.ngModule.viaModule === null) {
|
|
|
|
|
// Record the usage of an internal module as it needs to become an exported symbol
|
|
|
|
|
this.referencesRegistry.add(fn.ngModule.node, new Reference(fn.ngModule.node));
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 21:12:58 +01:00
|
|
|
// Only when processing the dts files do we need to determine which declaration to update.
|
|
|
|
|
if (this.processDts) {
|
|
|
|
|
const dtsFn = this.getDtsDeclarationForFunction(fn);
|
|
|
|
|
const typeParam = dtsFn.type && ts.isTypeReferenceNode(dtsFn.type) &&
|
|
|
|
|
dtsFn.type.typeArguments && dtsFn.type.typeArguments[0] ||
|
|
|
|
|
null;
|
|
|
|
|
if (!typeParam || isAnyKeyword(typeParam)) {
|
|
|
|
|
const ngModule = this.resolveNgModuleReference(fn);
|
|
|
|
|
const dtsFile = dtsFn.getSourceFile();
|
|
|
|
|
const analysis = analyses.has(dtsFile) ? analyses.get(dtsFile) : [];
|
|
|
|
|
analysis.push({declaration: dtsFn, ngModule});
|
|
|
|
|
analyses.set(dtsFile, analysis);
|
|
|
|
|
}
|
2018-12-07 13:10:52 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
return analyses;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getRootFiles(program: ts.Program): ts.SourceFile[] {
|
|
|
|
|
return program.getRootFileNames().map(f => program.getSourceFile(f)).filter(isDefined);
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-20 13:47:58 +00:00
|
|
|
private getDtsDeclarationForFunction(fn: ModuleWithProvidersFunction) {
|
2018-12-07 13:10:52 +00:00
|
|
|
let dtsFn: ts.Declaration|null = null;
|
2019-03-20 13:47:58 +00:00
|
|
|
const containerClass = fn.container && this.host.getClassSymbol(fn.container);
|
|
|
|
|
if (containerClass) {
|
fix(ngcc): consistently use outer declaration for classes (#32539)
In ngcc's reflection hosts for compiled JS bundles, such as ESM2015,
special care needs to be taken for classes as there may be an outer
declaration (referred to as "declaration") and an inner declaration
(referred to as "implementation") for a given class. Therefore, there
will also be two `ts.Symbol`s bound per class, and ngcc needs to switch
between those declarations and symbols depending on where certain
information can be found.
Prior to this commit, the `NgccReflectionHost` interface had methods
`getClassSymbol` and `findClassSymbols` that would return a `ts.Symbol`.
These class symbols would be used to kick off compilation of components
using ngtsc, so it is important for these symbols to correspond with the
publicly visible outer declaration of the class. However, the ESM2015
reflection host used to return the `ts.Symbol` for the inner
declaration, if the class was declared as follows:
```javascript
var MyClass = class MyClass {};
```
For the above code, `Esm2015ReflectionHost.getClassSymbol` would return
the `ts.Symbol` corresponding with the `class MyClass {}` declaration,
whereas it should have corresponded with the `var MyClass` declaration.
As a consequence, no `NgModule` could be resolved for the component, so
no components/directives would be in scope for the component. This
resulted in errors during runtime.
This commit resolves the issue by introducing a `NgccClassSymbol` that
contains references to both the outer and inner `ts.Symbol`, instead of
just a single `ts.Symbol`. This avoids the unclarity of whether a
`ts.Symbol` corresponds with the outer or inner declaration.
More details can be found here: https://hackmd.io/7nkgWOFWQlSRAuIW_8KPPw
Fixes #32078
Closes FW-1507
PR Close #32539
2019-09-03 21:26:58 +02:00
|
|
|
const dtsClass = this.host.getDtsDeclaration(containerClass.declaration.valueDeclaration);
|
2018-12-07 13:10:52 +00:00
|
|
|
// Get the declaration of the matching static method
|
|
|
|
|
dtsFn = dtsClass && ts.isClassDeclaration(dtsClass) ?
|
2020-04-06 08:30:08 +01:00
|
|
|
dtsClass.members.find(
|
|
|
|
|
member => ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) &&
|
|
|
|
|
member.name.text === fn.name) as ts.Declaration :
|
2018-12-07 13:10:52 +00:00
|
|
|
null;
|
|
|
|
|
} else {
|
2019-03-20 13:47:58 +00:00
|
|
|
dtsFn = this.host.getDtsDeclaration(fn.declaration);
|
2018-12-07 13:10:52 +00:00
|
|
|
}
|
|
|
|
|
if (!dtsFn) {
|
2019-03-20 13:47:58 +00:00
|
|
|
throw new Error(`Matching type declaration for ${fn.declaration.getText()} is missing`);
|
2018-12-07 13:10:52 +00:00
|
|
|
}
|
|
|
|
|
if (!isFunctionOrMethod(dtsFn)) {
|
2020-04-06 08:30:08 +01:00
|
|
|
throw new Error(`Matching type declaration for ${
|
|
|
|
|
fn.declaration.getText()} is not a function: ${dtsFn.getText()}`);
|
2018-12-07 13:10:52 +00:00
|
|
|
}
|
|
|
|
|
return dtsFn;
|
|
|
|
|
}
|
2019-09-29 23:26:41 +02:00
|
|
|
|
|
|
|
|
private resolveNgModuleReference(fn: ModuleWithProvidersFunction):
|
|
|
|
|
ConcreteDeclaration<ClassDeclaration> {
|
|
|
|
|
const ngModule = fn.ngModule;
|
|
|
|
|
|
|
|
|
|
// For external module references, use the declaration as is.
|
|
|
|
|
if (ngModule.viaModule !== null) {
|
|
|
|
|
return ngModule;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For internal (non-library) module references, redirect the module's value declaration
|
|
|
|
|
// to its type declaration.
|
|
|
|
|
const dtsNgModule = this.host.getDtsDeclaration(ngModule.node);
|
|
|
|
|
if (!dtsNgModule) {
|
2020-04-06 08:30:08 +01:00
|
|
|
throw new Error(`No typings declaration can be found for the referenced NgModule class in ${
|
|
|
|
|
fn.declaration.getText()}.`);
|
2019-09-29 23:26:41 +02:00
|
|
|
}
|
|
|
|
|
if (!ts.isClassDeclaration(dtsNgModule) || !hasNameIdentifier(dtsNgModule)) {
|
2020-04-06 08:30:08 +01:00
|
|
|
throw new Error(`The referenced NgModule in ${
|
|
|
|
|
fn.declaration
|
|
|
|
|
.getText()} is not a named class declaration in the typings program; instead we get ${
|
|
|
|
|
dtsNgModule.getText()}`);
|
2019-09-29 23:26:41 +02:00
|
|
|
}
|
|
|
|
|
|
2020-01-09 20:37:02 +01:00
|
|
|
return {node: dtsNgModule, known: null, viaModule: null};
|
2019-09-29 23:26:41 +02:00
|
|
|
}
|
2018-12-07 13:10:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function isFunctionOrMethod(declaration: ts.Declaration): declaration is ts.FunctionDeclaration|
|
|
|
|
|
ts.MethodDeclaration {
|
|
|
|
|
return ts.isFunctionDeclaration(declaration) || ts.isMethodDeclaration(declaration);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isAnyKeyword(typeParam: ts.TypeNode): typeParam is ts.KeywordTypeNode {
|
|
|
|
|
return typeParam.kind === ts.SyntaxKind.AnyKeyword;
|
|
|
|
|
}
|