|
| 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