Skip to content

Commit 620cd5c

Browse files
petebacondarwinjasonaden
authored andcommitted
feat(ivy): ngcc - implement CommonJsReflectionHost (angular#30200)
PR Close angular#30200
1 parent 76391f8 commit 620cd5c

File tree

2 files changed

+1952
-0
lines changed

2 files changed

+1952
-0
lines changed
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as ts from 'typescript';
10+
11+
import {Declaration, Import} from '../../../src/ngtsc/reflection';
12+
import {Logger} from '../logging/logger';
13+
import {BundleProgram} from '../packages/bundle_program';
14+
import {Esm5ReflectionHost} from './esm5_host';
15+
16+
export class CommonJsReflectionHost extends Esm5ReflectionHost {
17+
protected commonJsExports = new Map<ts.SourceFile, Map<string, Declaration>|null>();
18+
constructor(
19+
logger: Logger, isCore: boolean, protected program: ts.Program,
20+
protected compilerHost: ts.CompilerHost, dts?: BundleProgram|null) {
21+
super(logger, isCore, program.getTypeChecker(), dts);
22+
}
23+
24+
getImportOfIdentifier(id: ts.Identifier): Import|null {
25+
const requireCall = this.findCommonJsImport(id);
26+
if (requireCall === null) {
27+
return null;
28+
}
29+
return {from: requireCall.arguments[0].text, name: id.text};
30+
}
31+
32+
getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
33+
return this.getCommonJsImportedDeclaration(id) || super.getDeclarationOfIdentifier(id);
34+
}
35+
36+
getExportsOfModule(module: ts.Node): Map<string, Declaration>|null {
37+
return super.getExportsOfModule(module) || this.getCommonJsExports(module.getSourceFile());
38+
}
39+
40+
getCommonJsExports(sourceFile: ts.SourceFile): Map<string, Declaration>|null {
41+
if (!this.commonJsExports.has(sourceFile)) {
42+
const moduleExports = this.computeExportsOfCommonJsModule(sourceFile);
43+
this.commonJsExports.set(sourceFile, moduleExports);
44+
}
45+
return this.commonJsExports.get(sourceFile) !;
46+
}
47+
48+
private computeExportsOfCommonJsModule(sourceFile: ts.SourceFile): Map<string, Declaration> {
49+
const moduleMap = new Map<string, Declaration>();
50+
for (const statement of this.getModuleStatements(sourceFile)) {
51+
if (isCommonJsExportStatement(statement)) {
52+
const exportDeclaration = this.extractCommonJsExportDeclaration(statement);
53+
if (exportDeclaration !== null) {
54+
moduleMap.set(exportDeclaration.name, exportDeclaration.declaration);
55+
}
56+
} else if (isReexportStatement(statement)) {
57+
const reexports = this.extractCommonJsReexports(statement, sourceFile);
58+
for (const reexport of reexports) {
59+
moduleMap.set(reexport.name, reexport.declaration);
60+
}
61+
}
62+
}
63+
return moduleMap;
64+
}
65+
66+
private extractCommonJsExportDeclaration(statement: CommonJsExportStatement):
67+
CommonJsExportDeclaration|null {
68+
const exportExpression = statement.expression.right;
69+
const declaration = this.getDeclarationOfExpression(exportExpression);
70+
if (declaration === null) {
71+
return null;
72+
}
73+
const name = statement.expression.left.name.text;
74+
return {name, declaration};
75+
}
76+
77+
private extractCommonJsReexports(statement: ReexportStatement, containingFile: ts.SourceFile):
78+
CommonJsExportDeclaration[] {
79+
const reexports: CommonJsExportDeclaration[] = [];
80+
const requireCall = statement.expression.arguments[0];
81+
const importPath = requireCall.arguments[0].text;
82+
const importedFile = this.resolveModuleName(importPath, containingFile);
83+
if (importedFile !== undefined) {
84+
const viaModule = stripExtension(importedFile.fileName);
85+
const importedExports = this.getExportsOfModule(importedFile);
86+
if (importedExports !== null) {
87+
importedExports.forEach(
88+
(decl, name) => reexports.push({name, declaration: {node: decl.node, viaModule}}));
89+
}
90+
}
91+
return reexports;
92+
}
93+
94+
private findCommonJsImport(id: ts.Identifier): RequireCall|null {
95+
// Is `id` a namespaced property access, e.g. `Directive` in `core.Directive`?
96+
// If so capture the symbol of the namespace, e.g. `core`.
97+
const nsIdentifier = findNamespaceOfIdentifier(id);
98+
const nsSymbol = nsIdentifier && this.checker.getSymbolAtLocation(nsIdentifier) || null;
99+
const nsDeclaration = nsSymbol && nsSymbol.valueDeclaration;
100+
const initializer =
101+
nsDeclaration && ts.isVariableDeclaration(nsDeclaration) && nsDeclaration.initializer ||
102+
null;
103+
return initializer && isRequireCall(initializer) ? initializer : null;
104+
}
105+
106+
private getCommonJsImportedDeclaration(id: ts.Identifier): Declaration|null {
107+
const importInfo = this.getImportOfIdentifier(id);
108+
if (importInfo === null) {
109+
return null;
110+
}
111+
112+
const importedFile = this.resolveModuleName(importInfo.from, id.getSourceFile());
113+
if (importedFile === undefined) {
114+
return null;
115+
}
116+
117+
return {node: importedFile, viaModule: importInfo.from};
118+
}
119+
120+
private resolveModuleName(moduleName: string, containingFile: ts.SourceFile): ts.SourceFile
121+
|undefined {
122+
if (this.compilerHost.resolveModuleNames) {
123+
const moduleInfo =
124+
this.compilerHost.resolveModuleNames([moduleName], containingFile.fileName)[0];
125+
return moduleInfo && this.program.getSourceFile(moduleInfo.resolvedFileName);
126+
} else {
127+
const moduleInfo = ts.resolveModuleName(
128+
moduleName, containingFile.fileName, this.program.getCompilerOptions(),
129+
this.compilerHost);
130+
return moduleInfo.resolvedModule &&
131+
this.program.getSourceFile(moduleInfo.resolvedModule.resolvedFileName);
132+
}
133+
}
134+
}
135+
136+
type CommonJsExportStatement = ts.ExpressionStatement & {
137+
expression:
138+
ts.BinaryExpression & {left: ts.PropertyAccessExpression & {expression: ts.Identifier}}
139+
};
140+
export function isCommonJsExportStatement(s: ts.Statement): s is CommonJsExportStatement {
141+
return ts.isExpressionStatement(s) && ts.isBinaryExpression(s.expression) &&
142+
ts.isPropertyAccessExpression(s.expression.left) &&
143+
ts.isIdentifier(s.expression.left.expression) &&
144+
s.expression.left.expression.text === 'exports';
145+
}
146+
147+
interface CommonJsExportDeclaration {
148+
name: string;
149+
declaration: Declaration;
150+
}
151+
152+
export type RequireCall = ts.CallExpression & {arguments: [ts.StringLiteral]};
153+
export function isRequireCall(node: ts.Node): node is RequireCall {
154+
return ts.isCallExpression(node) && ts.isIdentifier(node.expression) &&
155+
node.expression.text === 'require' && node.arguments.length === 1 &&
156+
ts.isStringLiteral(node.arguments[0]);
157+
}
158+
159+
/**
160+
* If the identifier `id` is the RHS of a property access of the form `namespace.id`
161+
* and `namespace` is an identifer then return `namespace`, otherwise `null`.
162+
* @param id The identifier whose namespace we want to find.
163+
*/
164+
function findNamespaceOfIdentifier(id: ts.Identifier): ts.Identifier|null {
165+
return id.parent && ts.isPropertyAccessExpression(id.parent) &&
166+
ts.isIdentifier(id.parent.expression) ?
167+
id.parent.expression :
168+
null;
169+
}
170+
171+
export function stripParentheses(node: ts.Node): ts.Node {
172+
return ts.isParenthesizedExpression(node) ? node.expression : node;
173+
}
174+
175+
type ReexportStatement = ts.ExpressionStatement & {expression: {arguments: [RequireCall]}};
176+
function isReexportStatement(statement: ts.Statement): statement is ReexportStatement {
177+
return ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) &&
178+
ts.isIdentifier(statement.expression.expression) &&
179+
statement.expression.expression.text === '__export' &&
180+
statement.expression.arguments.length === 1 &&
181+
isRequireCall(statement.expression.arguments[0]);
182+
}
183+
184+
function stripExtension(fileName: string): string {
185+
return fileName.replace(/\..+$/, '');
186+
}

0 commit comments

Comments
 (0)