2019-04-29 09:54:30 +01: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-06-06 20:22:32 +01:00
|
|
|
import {absoluteFrom} from '../../../src/ngtsc/file_system';
|
2019-09-01 20:54:41 +02:00
|
|
|
import {Declaration, Import} from '../../../src/ngtsc/reflection';
|
2019-04-29 09:54:30 +01:00
|
|
|
import {Logger} from '../logging/logger';
|
|
|
|
|
import {BundleProgram} from '../packages/bundle_program';
|
2019-12-18 14:03:05 +00:00
|
|
|
import {isDefined, stripExtension} from '../utils';
|
2019-06-28 14:08:31 +01:00
|
|
|
|
2019-04-29 09:54:30 +01:00
|
|
|
import {Esm5ReflectionHost} from './esm5_host';
|
2019-09-01 20:54:41 +02:00
|
|
|
import {NgccClassSymbol} from './ngcc_host';
|
2019-04-29 09:54:30 +01:00
|
|
|
|
|
|
|
|
export class CommonJsReflectionHost extends Esm5ReflectionHost {
|
|
|
|
|
protected commonJsExports = new Map<ts.SourceFile, Map<string, Declaration>|null>();
|
2019-06-28 14:08:31 +01:00
|
|
|
protected topLevelHelperCalls = new Map<string, Map<ts.SourceFile, ts.CallExpression[]>>();
|
2019-12-18 14:03:04 +00:00
|
|
|
protected program: ts.Program;
|
|
|
|
|
protected compilerHost: ts.CompilerHost;
|
|
|
|
|
constructor(logger: Logger, isCore: boolean, src: BundleProgram, dts?: BundleProgram|null) {
|
|
|
|
|
super(logger, isCore, src, dts);
|
|
|
|
|
this.program = src.program;
|
|
|
|
|
this.compilerHost = src.host;
|
2019-04-29 09:54:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getImportOfIdentifier(id: ts.Identifier): Import|null {
|
2019-09-12 21:08:21 +02:00
|
|
|
const superImport = super.getImportOfIdentifier(id);
|
|
|
|
|
if (superImport !== null) {
|
|
|
|
|
return superImport;
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-29 09:54:30 +01:00
|
|
|
const requireCall = this.findCommonJsImport(id);
|
|
|
|
|
if (requireCall === null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return {from: requireCall.arguments[0].text, name: id.text};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
|
|
|
|
|
return this.getCommonJsImportedDeclaration(id) || super.getDeclarationOfIdentifier(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getExportsOfModule(module: ts.Node): Map<string, Declaration>|null {
|
|
|
|
|
return super.getExportsOfModule(module) || this.getCommonJsExports(module.getSourceFile());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getCommonJsExports(sourceFile: ts.SourceFile): Map<string, Declaration>|null {
|
2019-06-28 14:08:31 +01:00
|
|
|
return getOrDefault(
|
|
|
|
|
this.commonJsExports, sourceFile, () => this.computeExportsOfCommonJsModule(sourceFile));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Search statements related to the given class for calls to the specified helper.
|
|
|
|
|
*
|
|
|
|
|
* In CommonJS these helper calls can be outside the class's IIFE at the top level of the
|
|
|
|
|
* source file. Searching the top level statements for helpers can be expensive, so we
|
|
|
|
|
* try to get helpers from the IIFE first and only fall back on searching the top level if
|
|
|
|
|
* no helpers are found.
|
|
|
|
|
*
|
|
|
|
|
* @param classSymbol the class whose helper calls we are interested in.
|
|
|
|
|
* @param helperName the name of the helper (e.g. `__decorate`) whose calls we are interested in.
|
|
|
|
|
* @returns an array of nodes of calls to the helper with the given name.
|
|
|
|
|
*/
|
2019-11-08 11:37:08 +00:00
|
|
|
protected getHelperCallsForClass(classSymbol: NgccClassSymbol, helperNames: string[]):
|
2019-06-28 14:08:31 +01:00
|
|
|
ts.CallExpression[] {
|
2019-11-08 11:37:08 +00:00
|
|
|
const esm5HelperCalls = super.getHelperCallsForClass(classSymbol, helperNames);
|
2019-06-28 14:08:31 +01:00
|
|
|
if (esm5HelperCalls.length > 0) {
|
|
|
|
|
return esm5HelperCalls;
|
|
|
|
|
} else {
|
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 sourceFile = classSymbol.declaration.valueDeclaration.getSourceFile();
|
2019-11-08 11:37:08 +00:00
|
|
|
return this.getTopLevelHelperCalls(sourceFile, helperNames);
|
2019-04-29 09:54:30 +01:00
|
|
|
}
|
2019-06-28 14:08:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Find all the helper calls at the top level of a source file.
|
|
|
|
|
*
|
|
|
|
|
* We cache the helper calls per source file so that we don't have to keep parsing the code for
|
|
|
|
|
* each class in a file.
|
|
|
|
|
*
|
|
|
|
|
* @param sourceFile the source who may contain helper calls.
|
2019-11-08 11:37:08 +00:00
|
|
|
* @param helperNames the names of the helpers (e.g. `__decorate`) whose calls we are interested
|
|
|
|
|
* in.
|
2019-06-28 14:08:31 +01:00
|
|
|
* @returns an array of nodes of calls to the helper with the given name.
|
|
|
|
|
*/
|
2019-11-08 11:37:08 +00:00
|
|
|
private getTopLevelHelperCalls(sourceFile: ts.SourceFile, helperNames: string[]):
|
2019-06-28 14:08:31 +01:00
|
|
|
ts.CallExpression[] {
|
2019-11-08 11:37:08 +00:00
|
|
|
const calls: ts.CallExpression[] = [];
|
|
|
|
|
helperNames.forEach(helperName => {
|
|
|
|
|
const helperCallsMap = getOrDefault(this.topLevelHelperCalls, helperName, () => new Map());
|
|
|
|
|
calls.push(...getOrDefault(
|
|
|
|
|
helperCallsMap, sourceFile,
|
|
|
|
|
() => sourceFile.statements.map(statement => this.getHelperCall(statement, helperNames))
|
|
|
|
|
.filter(isDefined)));
|
|
|
|
|
});
|
|
|
|
|
return calls;
|
2019-04-29 09:54:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private computeExportsOfCommonJsModule(sourceFile: ts.SourceFile): Map<string, Declaration> {
|
|
|
|
|
const moduleMap = new Map<string, Declaration>();
|
|
|
|
|
for (const statement of this.getModuleStatements(sourceFile)) {
|
|
|
|
|
if (isCommonJsExportStatement(statement)) {
|
|
|
|
|
const exportDeclaration = this.extractCommonJsExportDeclaration(statement);
|
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
|
|
|
moduleMap.set(exportDeclaration.name, exportDeclaration.declaration);
|
2019-04-29 09:54:30 +01:00
|
|
|
} else if (isReexportStatement(statement)) {
|
|
|
|
|
const reexports = this.extractCommonJsReexports(statement, sourceFile);
|
|
|
|
|
for (const reexport of reexports) {
|
|
|
|
|
moduleMap.set(reexport.name, reexport.declaration);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return moduleMap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extractCommonJsExportDeclaration(statement: CommonJsExportStatement):
|
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
|
|
|
CommonJsExportDeclaration {
|
2019-04-29 09:54:30 +01:00
|
|
|
const exportExpression = statement.expression.right;
|
|
|
|
|
const declaration = this.getDeclarationOfExpression(exportExpression);
|
|
|
|
|
const name = statement.expression.left.name.text;
|
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
|
|
|
if (declaration !== null) {
|
|
|
|
|
return {name, declaration};
|
|
|
|
|
} else {
|
|
|
|
|
return {
|
|
|
|
|
name,
|
|
|
|
|
declaration: {
|
|
|
|
|
node: null,
|
|
|
|
|
expression: exportExpression,
|
|
|
|
|
viaModule: null,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
2019-04-29 09:54:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extractCommonJsReexports(statement: ReexportStatement, containingFile: ts.SourceFile):
|
|
|
|
|
CommonJsExportDeclaration[] {
|
2019-12-18 14:03:04 +00:00
|
|
|
const reexportArg = statement.expression.arguments[0];
|
|
|
|
|
|
|
|
|
|
const requireCall = isRequireCall(reexportArg) ?
|
|
|
|
|
reexportArg :
|
|
|
|
|
ts.isIdentifier(reexportArg) ? this.findRequireCallReference(reexportArg) : null;
|
|
|
|
|
if (requireCall === null) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-29 09:54:30 +01:00
|
|
|
const importPath = requireCall.arguments[0].text;
|
|
|
|
|
const importedFile = this.resolveModuleName(importPath, containingFile);
|
2019-12-18 14:03:04 +00:00
|
|
|
if (importedFile === undefined) {
|
|
|
|
|
return [];
|
2019-04-29 09:54:30 +01:00
|
|
|
}
|
2019-12-18 14:03:04 +00:00
|
|
|
|
|
|
|
|
const importedExports = this.getExportsOfModule(importedFile);
|
|
|
|
|
if (importedExports === null) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-18 14:03:05 +00:00
|
|
|
const viaModule = stripExtension(importedFile.fileName);
|
2019-12-18 14:03:04 +00:00
|
|
|
const reexports: CommonJsExportDeclaration[] = [];
|
|
|
|
|
importedExports.forEach((decl, name) => {
|
|
|
|
|
if (decl.node !== null) {
|
|
|
|
|
reexports.push({name, declaration: {node: decl.node, viaModule}});
|
|
|
|
|
} else {
|
|
|
|
|
reexports.push({name, declaration: {node: null, expression: decl.expression, viaModule}});
|
|
|
|
|
}
|
|
|
|
|
});
|
2019-04-29 09:54:30 +01:00
|
|
|
return reexports;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private findCommonJsImport(id: ts.Identifier): RequireCall|null {
|
|
|
|
|
// Is `id` a namespaced property access, e.g. `Directive` in `core.Directive`?
|
|
|
|
|
// If so capture the symbol of the namespace, e.g. `core`.
|
|
|
|
|
const nsIdentifier = findNamespaceOfIdentifier(id);
|
2019-12-18 14:03:04 +00:00
|
|
|
return nsIdentifier && this.findRequireCallReference(nsIdentifier);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private findRequireCallReference(id: ts.Identifier): RequireCall|null {
|
|
|
|
|
const symbol = id && this.checker.getSymbolAtLocation(id) || null;
|
|
|
|
|
const declaration = symbol && symbol.valueDeclaration;
|
2019-04-29 09:54:30 +01:00
|
|
|
const initializer =
|
2019-12-18 14:03:04 +00:00
|
|
|
declaration && ts.isVariableDeclaration(declaration) && declaration.initializer || null;
|
2019-04-29 09:54:30 +01:00
|
|
|
return initializer && isRequireCall(initializer) ? initializer : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getCommonJsImportedDeclaration(id: ts.Identifier): Declaration|null {
|
|
|
|
|
const importInfo = this.getImportOfIdentifier(id);
|
|
|
|
|
if (importInfo === null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const importedFile = this.resolveModuleName(importInfo.from, id.getSourceFile());
|
|
|
|
|
if (importedFile === undefined) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-02 10:30:53 +03:00
|
|
|
const viaModule = !importInfo.from.startsWith('.') ? importInfo.from : null;
|
|
|
|
|
return {node: importedFile, viaModule};
|
2019-04-29 09:54:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private resolveModuleName(moduleName: string, containingFile: ts.SourceFile): ts.SourceFile
|
|
|
|
|
|undefined {
|
|
|
|
|
if (this.compilerHost.resolveModuleNames) {
|
2019-10-01 16:44:50 -07:00
|
|
|
const moduleInfo = this.compilerHost.resolveModuleNames(
|
|
|
|
|
[moduleName], containingFile.fileName, undefined, undefined,
|
|
|
|
|
this.program.getCompilerOptions())[0];
|
2019-06-06 20:22:32 +01:00
|
|
|
return moduleInfo && this.program.getSourceFile(absoluteFrom(moduleInfo.resolvedFileName));
|
2019-04-29 09:54:30 +01:00
|
|
|
} else {
|
|
|
|
|
const moduleInfo = ts.resolveModuleName(
|
|
|
|
|
moduleName, containingFile.fileName, this.program.getCompilerOptions(),
|
|
|
|
|
this.compilerHost);
|
|
|
|
|
return moduleInfo.resolvedModule &&
|
2019-06-06 20:22:32 +01:00
|
|
|
this.program.getSourceFile(absoluteFrom(moduleInfo.resolvedModule.resolvedFileName));
|
2019-04-29 09:54:30 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CommonJsExportStatement = ts.ExpressionStatement & {
|
|
|
|
|
expression:
|
|
|
|
|
ts.BinaryExpression & {left: ts.PropertyAccessExpression & {expression: ts.Identifier}}
|
|
|
|
|
};
|
|
|
|
|
export function isCommonJsExportStatement(s: ts.Statement): s is CommonJsExportStatement {
|
|
|
|
|
return ts.isExpressionStatement(s) && ts.isBinaryExpression(s.expression) &&
|
|
|
|
|
ts.isPropertyAccessExpression(s.expression.left) &&
|
|
|
|
|
ts.isIdentifier(s.expression.left.expression) &&
|
|
|
|
|
s.expression.left.expression.text === 'exports';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface CommonJsExportDeclaration {
|
|
|
|
|
name: string;
|
|
|
|
|
declaration: Declaration;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type RequireCall = ts.CallExpression & {arguments: [ts.StringLiteral]};
|
|
|
|
|
export function isRequireCall(node: ts.Node): node is RequireCall {
|
|
|
|
|
return ts.isCallExpression(node) && ts.isIdentifier(node.expression) &&
|
|
|
|
|
node.expression.text === 'require' && node.arguments.length === 1 &&
|
|
|
|
|
ts.isStringLiteral(node.arguments[0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* If the identifier `id` is the RHS of a property access of the form `namespace.id`
|
|
|
|
|
* and `namespace` is an identifer then return `namespace`, otherwise `null`.
|
|
|
|
|
* @param id The identifier whose namespace we want to find.
|
|
|
|
|
*/
|
|
|
|
|
function findNamespaceOfIdentifier(id: ts.Identifier): ts.Identifier|null {
|
|
|
|
|
return id.parent && ts.isPropertyAccessExpression(id.parent) &&
|
|
|
|
|
ts.isIdentifier(id.parent.expression) ?
|
|
|
|
|
id.parent.expression :
|
|
|
|
|
null;
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-18 14:03:04 +00:00
|
|
|
type ReexportStatement = ts.ExpressionStatement & {expression: ts.CallExpression};
|
2019-04-29 09:54:30 +01:00
|
|
|
function isReexportStatement(statement: ts.Statement): statement is ReexportStatement {
|
|
|
|
|
return ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) &&
|
|
|
|
|
ts.isIdentifier(statement.expression.expression) &&
|
|
|
|
|
statement.expression.expression.text === '__export' &&
|
2019-12-18 14:03:04 +00:00
|
|
|
statement.expression.arguments.length === 1;
|
2019-04-29 09:54:30 +01:00
|
|
|
}
|
|
|
|
|
|
2019-06-28 14:08:31 +01:00
|
|
|
function getOrDefault<K, V>(map: Map<K, V>, key: K, factory: (key: K) => V): V {
|
|
|
|
|
if (!map.has(key)) {
|
|
|
|
|
map.set(key, factory(key));
|
|
|
|
|
}
|
|
|
|
|
return map.get(key) !;
|
2019-09-01 20:54:41 +02:00
|
|
|
}
|