Skip to content
5 changes: 2 additions & 3 deletions packages/compiler-cli/src/ngcc/src/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class Analyzer {
analyzeFile(file: ParsedFile): AnalyzedFile {
const constantPool = new ConstantPool();
const analyzedClasses =
file.decoratedClasses.map(clazz => this.analyzeClass(file.sourceFile, constantPool, clazz))
file.decoratedClasses.map(clazz => this.analyzeClass(constantPool, clazz))
.filter(isDefined);

return {
Expand All @@ -75,8 +75,7 @@ export class Analyzer {
};
}

protected analyzeClass(file: ts.SourceFile, pool: ConstantPool, clazz: ParsedClass): AnalyzedClass
|undefined {
protected analyzeClass(pool: ConstantPool, clazz: ParsedClass): AnalyzedClass|undefined {
const matchingHandlers = this.handlers
.map(handler => ({
handler,
Expand Down
18 changes: 9 additions & 9 deletions packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@

import * as ts from 'typescript';

import {ClassMember, ClassMemberKind, Decorator, Parameter} from '../../../ngtsc/host';
import {ClassMember, ClassMemberKind, CtorParameter, Decorator} from '../../../ngtsc/host';
import {TypeScriptReflectionHost, reflectObjectLiteral} from '../../../ngtsc/metadata';
import {getNameText} from '../utils';

import {NgccReflectionHost} from './ngcc_host';

export const DECORATORS = 'decorators' as ts.__String;
Expand Down Expand Up @@ -162,15 +163,15 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
*
* @throws if `declaration` does not resolve to a class declaration.
*/
getConstructorParameters(clazz: ts.Declaration): Parameter[]|null {
getConstructorParameters(clazz: ts.Declaration): CtorParameter[]|null {
const classSymbol = this.getClassSymbol(clazz);
if (!classSymbol) {
throw new Error(
`Attempted to get constructor parameters of a non-class: "${clazz.getText()}"`);
}
const parameterNodes = this.getConstructorParameterDeclarations(classSymbol);
if (parameterNodes) {
const parameters: Parameter[] = [];
const parameters: CtorParameter[] = [];
const decoratorInfo = this.getConstructorDecorators(classSymbol);
parameterNodes.forEach((node, index) => {
const info = decoratorInfo[index];
Expand All @@ -187,12 +188,11 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
}

/**
* Find a symbol for a declaration that we think is a class.
* @param declaration The declaration whose symbol we are finding
* @returns the symbol for the declaration or `undefined` if it is not
* a "class" or has no symbol.
* Find a symbol for a node that we think is a class.
* @param node The node whose symbol we are finding.
* @returns The symbol for the node or `undefined` if it is not a "class" or has no symbol.
*/
getClassSymbol(declaration: ts.Declaration): ts.Symbol|undefined {
getClassSymbol(declaration: ts.Node): ts.Symbol|undefined {
return ts.isClassDeclaration(declaration) ?
declaration.name && this.checker.getSymbolAtLocation(declaration.name) :
undefined;
Expand Down Expand Up @@ -422,4 +422,4 @@ function isNamedDeclaration(node: ts.Declaration): node is ts.NamedDeclaration {
function isClassMemberType(node: ts.Declaration): node is ts.ClassElement|
ts.PropertyAccessExpression|ts.BinaryExpression {
return ts.isClassElement(node) || isPropertyAccess(node) || ts.isBinaryExpression(node);
}
}
141 changes: 126 additions & 15 deletions packages/compiler-cli/src/ngcc/src/host/esm5_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
*/

import * as ts from 'typescript';
import {ClassMember, ClassMemberKind, Decorator} from '../../../ngtsc/host';
import {ClassMember, ClassMemberKind, Decorator, FunctionDefinition, Parameter} from '../../../ngtsc/host';
import {reflectObjectLiteral} from '../../../ngtsc/metadata';
import {getNameText} from '../utils';
import {CONSTRUCTOR_PARAMS, Esm2015ReflectionHost, getPropertyValueFromSymbol} from './esm2015_host';

/**
Expand All @@ -32,28 +33,90 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
constructor(checker: ts.TypeChecker) { super(checker); }

/**
* Check whether the given declaration node actually represents a class.
* Check whether the given node actually represents a class.
*/
isClass(node: ts.Declaration): boolean { return !!this.getClassSymbol(node); }
isClass(node: ts.Node): boolean { return super.isClass(node) || !!this.getClassSymbol(node); }

/**
* In ESM5 the implementation of a class is a function expression that is hidden inside an IIFE.
* Find a symbol for a node that we think is a class.
*
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE.
* So we need to dig around inside to get hold of the "class" symbol.
* @param declaration the top level declaration that represents an exported class.
*
* `node` might be one of:
* - A class declaration (from a declaration file).
* - The declaration of the outer variable, which is assigned the result of the IIFE.
* - The function declaration inside the IIFE, which is eventually returned and assigned to the
* outer variable.
*
* @param node The top level declaration that represents an exported class or the function
* expression inside the IIFE.
* @returns The symbol for the node or `undefined` if it is not a "class" or has no symbol.
*/
getClassSymbol(declaration: ts.Declaration): ts.Symbol|undefined {
if (ts.isVariableDeclaration(declaration)) {
const iifeBody = getIifeBody(declaration);
if (iifeBody) {
const innerClassIdentifier = getReturnIdentifier(iifeBody);
if (innerClassIdentifier) {
return this.checker.getSymbolAtLocation(innerClassIdentifier);
}
}
getClassSymbol(node: ts.Node): ts.Symbol|undefined {
const symbol = super.getClassSymbol(node);
if (symbol) return symbol;

if (ts.isVariableDeclaration(node)) {
const iifeBody = getIifeBody(node);
if (!iifeBody) return undefined;

const innerClassIdentifier = getReturnIdentifier(iifeBody);
if (!innerClassIdentifier) return undefined;

return this.checker.getSymbolAtLocation(innerClassIdentifier);
} else if (ts.isFunctionDeclaration(node)) {
// It might be the function expression inside the IIFE. We need to go 5 levels up...

// 1. IIFE body.
let outerNode = node.parent;
if (!outerNode || !ts.isBlock(outerNode)) return undefined;

// 2. IIFE function expression.
outerNode = outerNode.parent;
if (!outerNode || !ts.isFunctionExpression(outerNode)) return undefined;

// 3. IIFE call expression.
outerNode = outerNode.parent;
if (!outerNode || !ts.isCallExpression(outerNode)) return undefined;

// 4. Parenthesis around IIFE.
outerNode = outerNode.parent;
if (!outerNode || !ts.isParenthesizedExpression(outerNode)) return undefined;

// 5. Outer variable declaration.
outerNode = outerNode.parent;
if (!outerNode || !ts.isVariableDeclaration(outerNode)) return undefined;

return this.getClassSymbol(outerNode);
}

return undefined;
}

/**
* Parse a function declaration to find the relevant metadata about it.
* In ESM5 we need to do special work with optional arguments to the function, since they get
* their own initializer statement that needs to be parsed and then not included in the "body"
* statements of the function.
* @param node the function declaration to parse.
*/
getDefinitionOfFunction<T extends ts.FunctionDeclaration|ts.MethodDeclaration|
ts.FunctionExpression>(node: T): FunctionDefinition<T> {
const parameters =
node.parameters.map(p => ({name: getNameText(p.name), node: p, initializer: null}));
let lookingForParamInitializers = true;

const statements = node.body && node.body.statements.filter(s => {
lookingForParamInitializers =
lookingForParamInitializers && reflectParamInitializer(s, parameters);
// If we are no longer looking for parameter initializers then we include this statement
return !lookingForParamInitializers;
});

return {node, body: statements || null, parameters};
}

/**
* Find the declarations of the constructor parameters of a class identified by its symbol.
* In ESM5 there is no "class" so the constructor that we want is actually the declaration
Expand Down Expand Up @@ -134,4 +197,52 @@ function getReturnStatement(declaration: ts.Expression | undefined): ts.ReturnSt

function reflectArrayElement(element: ts.Expression) {
return ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null;
}
}

/**
* Parse the statement to extract the ESM5 parameter initializer if there is one.
* If one is found, add it to the appropriate parameter in the `parameters` collection.
*
* The form we are looking for is:
*
* ```
* if (arg === void 0) { arg = initializer; }
* ```
*
* @param statement A statement that may be initializing an optional parameter
* @param parameters The collection of parameters that were found in the function definition
* @returns true if the statement was a parameter initializer
*/
function reflectParamInitializer(statement: ts.Statement, parameters: Parameter[]) {
if (ts.isIfStatement(statement) && isUndefinedComparison(statement.expression) &&
ts.isBlock(statement.thenStatement) && statement.thenStatement.statements.length === 1) {
const ifStatementComparison = statement.expression; // (arg === void 0)
const thenStatement = statement.thenStatement.statements[0]; // arg = initializer;
if (isAssignment(thenStatement)) {
const comparisonName = ifStatementComparison.left.text;
const assignmentName = thenStatement.expression.left.text;
if (comparisonName === assignmentName) {
const parameter = parameters.find(p => p.name === comparisonName);
if (parameter) {
parameter.initializer = thenStatement.expression.right;
return true;
}
}
}
}
return false;
}

function isUndefinedComparison(expression: ts.Expression): expression is ts.Expression&
{left: ts.Identifier, right: ts.Expression} {
return ts.isBinaryExpression(expression) &&
expression.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken &&
ts.isVoidExpression(expression.right) && ts.isIdentifier(expression.left);
}

function isAssignment(statement: ts.Statement): statement is ts.ExpressionStatement&
{expression: {left: ts.Identifier, right: ts.Expression}} {
return ts.isExpressionStatement(statement) && ts.isBinaryExpression(statement.expression) &&
statement.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
ts.isIdentifier(statement.expression.left);
}
2 changes: 1 addition & 1 deletion packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ import {ReflectionHost} from '../../../ngtsc/host';
* A reflection host that has extra methods for looking at non-Typescript package formats
*/
export interface NgccReflectionHost extends ReflectionHost {
getClassSymbol(declaration: ts.Declaration): ts.Symbol|undefined;
getClassSymbol(node: ts.Node): ts.Symbol|undefined;
}
5 changes: 3 additions & 2 deletions packages/compiler-cli/src/ngcc/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import {PackageTransformer} from './transform/package_transformer';

export function mainNgcc(args: string[]): number {
const packagePath = resolve(args[0]);
const format = args[1] || 'fesm2015';

// TODO: find all the package tyoes to transform
// TODO: find all the package types to transform
// TODO: error/warning logging/handling etc

const transformer = new PackageTransformer();
transformer.transform(packagePath, 'fesm2015');
transformer.transform(packagePath, format);

return 0;
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,12 @@ export class PackageTransformer {
const targetNodeModules = sourceNodeModules.replace(/node_modules$/, 'node_modules_ngtsc');
const entryPointPaths = getEntryPoints(packagePath, format);
entryPointPaths.forEach(entryPointPath => {
const options: ts.CompilerOptions = {allowJs: true, rootDir: entryPointPath};
const options: ts.CompilerOptions = {
allowJs: true,
maxNodeModuleJsDepth: Infinity,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch on this one.

rootDir: entryPointPath,
};

const host = ts.createCompilerHost(options);
const packageProgram = ts.createProgram([entryPointPath], options, host);
const entryPointFile = packageProgram.getSourceFile(entryPointPath) !;
Expand Down Expand Up @@ -76,6 +81,7 @@ export class PackageTransformer {
case 'esm2015':
case 'fesm2015':
return new Esm2015ReflectionHost(program.getTypeChecker());
case 'esm5':
case 'fesm5':
return new Esm5ReflectionHost(program.getTypeChecker());
default:
Expand All @@ -88,6 +94,7 @@ export class PackageTransformer {
case 'esm2015':
case 'fesm2015':
return new Esm2015FileParser(program, host);
case 'esm5':
case 'fesm5':
return new Esm5FileParser(program, host);
default:
Expand All @@ -100,6 +107,7 @@ export class PackageTransformer {
case 'esm2015':
case 'fesm2015':
return new Esm2015Renderer(host);
case 'esm5':
case 'fesm5':
return new Esm5Renderer(host);
default:
Expand Down
Loading