1/* 2 * Copyright (c) 2025 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import * as ts from 'typescript'; 17import path from 'path'; 18 19import { toUnixPath } from './utils'; 20import { sdkConfigPrefix } from '../main'; 21 22interface ImportInfo { 23 defaultImport?: { 24 name: ts.Identifier; 25 }; 26 namedImports: ts.ImportSpecifier[]; 27} 28 29interface SymbolInfo { 30 filePath: string, 31 isDefault: boolean, 32 exportName?: string 33} 34 35export function expandAllImportPaths(checker: ts.TypeChecker, rollupObejct: Object): Function { 36 const expandImportPath: Object = rollupObejct.share.projectConfig?.expandImportPath; 37 if (!(expandImportPath && Object.entries(expandImportPath).length !== 0) || !expandImportPath.enable) { 38 return () => sourceFile => sourceFile; 39 } 40 const exclude: string[] = expandImportPath?.exclude ? expandImportPath?.exclude : []; 41 return (context: ts.TransformationContext) => { 42 // @ts-ignore 43 const visitor: ts.Visitor = (node: ts.Node): ts.VisitResult<ts.Node> => { 44 if (ts.isImportDeclaration(node)) { 45 const result: ts.ImportDeclaration[] = transformImportDecl(node, checker, exclude, 46 Object.assign(rollupObejct.share.projectConfig, rollupObejct.share.arkProjectConfig)); 47 return result.length > 0 ? result : node; 48 } 49 return node; 50 }; 51 52 return (node: ts.SourceFile): ts.SourceFile => { 53 return ts.visitEachChild(node, visitor, context); 54 }; 55 }; 56} 57 58function transformImportDecl(node: ts.ImportDeclaration, checker: ts.TypeChecker, exclude: string[], 59 projectConfig: Object): ts.ImportDeclaration[] { 60 const moduleSpecifier: ts.StringLiteral = node.moduleSpecifier as ts.StringLiteral; 61 const moduleRequest: string = moduleSpecifier.text; 62 const REG_SYSTEM_MODULE: RegExp = new RegExp(`@(${sdkConfigPrefix})\\.(\\S+)`); 63 const REG_LIB_SO: RegExp = /lib(\S+)\.so/; 64 const depName2DepInfo: Object = projectConfig.depName2DepInfo; 65 const packageDir: string = projectConfig.packageDir; 66 const hspNameOhmMap: Object = Object.assign({}, projectConfig.hspNameOhmMap, projectConfig.harNameOhmMap); 67 if (moduleRequest.startsWith('.') || REG_SYSTEM_MODULE.test(moduleRequest.trim()) || REG_LIB_SO.test(moduleRequest.trim()) || 68 exclude.indexOf(moduleRequest) !== -1 || (depName2DepInfo && !(depName2DepInfo.has(moduleRequest))) || 69 (hspNameOhmMap && hspNameOhmMap[moduleRequest])) { 70 return []; 71 } 72 const importClause = node.importClause; 73 if (!importClause) { 74 return []; 75 } 76 if ((importClause.namedBindings && ts.isNamespaceImport(importClause.namedBindings)) || importClause.isTypeOnly) { 77 return []; 78 } 79 80 const importMap = new Map<string, ImportInfo>(); 81 // default import 82 processDefaultImport(checker, importMap, importClause, moduleSpecifier, packageDir, depName2DepInfo); 83 // named imports 84 processNamedImport(checker, importMap, importClause, moduleSpecifier, packageDir, depName2DepInfo); 85 if (importMap.size === 0) { 86 return []; 87 } 88 const results: ts.ImportDeclaration[] = []; 89 90 for (const [filePath, info] of importMap.entries()) { 91 let realModuleRequest: string = filePath; 92 if (filePath.endsWith('.ets') || filePath.endsWith('.ts')) { 93 realModuleRequest = filePath.replace(/\.(ets|ts)$/, ''); 94 } 95 results.push(createImportDeclarationFromInfo(info, node, realModuleRequest)); 96 } 97 98 return results; 99} 100 101 102function processDefaultImport(checker: ts.TypeChecker, importMap: Map<string, ImportInfo>, importClause: ts.ImportClause, 103 moduleSpecifier: ts.StringLiteral, packageDir: string, depName2DepInfo: Object): void { 104 if (importClause.name) { 105 const resolved = getRealFilePath(checker, moduleSpecifier, 'default', packageDir, depName2DepInfo); 106 if (!resolved) { 107 return; 108 } 109 const { filePath, isDefault, exportName } = resolved; 110 111 if (!importMap.has(filePath)) { 112 importMap.set(filePath, { namedImports: [] }); 113 } 114 if (isDefault) { 115 importMap.get(filePath)!.defaultImport = { 116 name: importClause.name 117 }; 118 } else { 119 // fallback: was re-exported as default, but originally named 120 importMap.get(filePath)!.namedImports.push( 121 ts.factory.createImportSpecifier(importClause.isTypeOnly, 122 exportName && (exportName !== importClause.name.text) ? 123 ts.factory.createIdentifier(exportName) : ts.factory.createIdentifier(importClause.name.text), 124 importClause.name) 125 ); 126 } 127 } 128} 129 130function processNamedImport(checker: ts.TypeChecker, importMap: Map<string, ImportInfo>, importClause: ts.ImportClause, 131 moduleSpecifier: ts.StringLiteral, packageDir: string, depName2DepInfo: Object): void { 132 if (importClause.namedBindings && ts.isNamedImports(importClause.namedBindings)) { 133 for (const element of importClause.namedBindings.elements) { 134 const name: string = element.propertyName?.text || element.name.text; 135 const resolved: SymbolInfo | undefined = getRealFilePath(checker, moduleSpecifier, name, packageDir, depName2DepInfo); 136 if (!resolved) { 137 continue; 138 } 139 let { filePath, isDefault, exportName } = resolved; 140 if (element.isTypeOnly) { 141 filePath = moduleSpecifier.text; 142 } 143 144 if (!importMap.has(filePath)) { 145 importMap.set(filePath, { namedImports: [] }); 146 } 147 if (isDefault) { 148 importMap.get(filePath)!.defaultImport = { 149 name: element.name 150 }; 151 } else { 152 importMap.get(filePath)!.namedImports.push( 153 ts.factory.createImportSpecifier(element.isTypeOnly, 154 exportName && (exportName !== name) ? 155 ts.factory.createIdentifier(exportName) : element.propertyName, 156 element.name) 157 ); 158 } 159 } 160 } 161} 162 163function createImportDeclarationFromInfo(importInfo: ImportInfo, originalNode: ts.ImportDeclaration, 164 modulePath: string): ts.ImportDeclaration { 165 const importClause = ts.factory.createImportClause(false, importInfo.defaultImport?.name, 166 importInfo.namedImports.length > 0 ? ts.factory.createNamedImports(importInfo.namedImports) : undefined); 167 168 // @ts-ignore 169 importClause.isLazy = originalNode.importClause?.isLazy; 170 171 return ts.factory.updateImportDeclaration(originalNode, originalNode.modifiers, importClause, 172 ts.factory.createStringLiteral(modulePath), originalNode.assertClause); 173} 174 175function genModuleRequest(filePath: string, moduleRequest: string, depName2DepInfo: Object): string { 176 const unixFilePath: string = toUnixPath(filePath); 177 for (const [depName, depInfo] of depName2DepInfo) { 178 const unixModuleRootPath: string = toUnixPath(depInfo.pkgRootPath); 179 if (unixFilePath.startsWith(unixModuleRootPath + '/') && depName === moduleRequest) { 180 return unixFilePath.replace(unixModuleRootPath, moduleRequest); 181 } 182 } 183 return moduleRequest; 184} 185 186function getRealFilePath(checker: ts.TypeChecker, moduleSpecifier: ts.StringLiteral, 187 importName: string, packageDir: string, depName2DepInfo: Object): SymbolInfo | undefined { 188 const symbol: ts.Symbol | undefined = resolveImportedSymbol(checker, moduleSpecifier, importName); 189 if (!symbol) { 190 return undefined; 191 } 192 193 const finalSymbol: ts.Symbol = resolveAliasedSymbol(symbol, checker); 194 if (!finalSymbol || !finalSymbol.declarations || finalSymbol.declarations.length === 0) { 195 return { 196 filePath: moduleSpecifier.text, 197 isDefault: importName === 'default', 198 }; 199 } 200 201 const decl: ts.Declaration = finalSymbol.declarations?.[0]; 202 const filePath: string = path.normalize(decl.getSourceFile().fileName); 203 const newFilePath: string = genModuleRequest(filePath, moduleSpecifier.text, depName2DepInfo); 204 if (filePath.indexOf(packageDir) !== -1 || filePath.endsWith('.d.ets') || filePath.endsWith('.d.ts') || newFilePath === moduleSpecifier.text) { 205 return { 206 filePath: moduleSpecifier.text, 207 isDefault: importName === 'default', 208 }; 209 } 210 const [isDefault, exportName] = getDefaultExportName(finalSymbol); 211 if (!isDefault && !exportName) { 212 return { 213 filePath: moduleSpecifier.text, 214 isDefault: importName === 'default', 215 }; 216 } 217 return { filePath: newFilePath, isDefault, exportName }; 218} 219 220function resolveImportedSymbol(checker: ts.TypeChecker, moduleSpecifier: ts.StringLiteral, 221 exportName: string): ts.Symbol | undefined { 222 const moduleSymbol: ts.Symbol = checker.getSymbolAtLocation(moduleSpecifier); 223 if (!moduleSymbol) { 224 return undefined; 225 } 226 227 const exports: ts.Symbol[] = checker.getExportsOfModule(moduleSymbol); 228 if (!exports) { 229 return undefined; 230 } 231 232 for (const sym of exports) { 233 const name: string = sym.escapedName.toString(); 234 if (name === exportName) { 235 return sym; 236 } 237 } 238 return undefined; 239} 240 241function resolveAliasedSymbol(symbol: ts.Symbol, checker: ts.TypeChecker): ts.Symbol { 242 const visited = new Set<ts.Symbol>(); 243 let finalSymbol: ts.Symbol | undefined = symbol; 244 245 while (finalSymbol && finalSymbol.flags & ts.SymbolFlags.Alias) { 246 if (visited.has(finalSymbol)) { 247 break; 248 } 249 visited.add(finalSymbol); 250 const aliased = checker.getAliasedSymbol(finalSymbol); 251 if (!aliased) { 252 break; 253 } 254 finalSymbol = aliased; 255 } 256 257 // fallback: skip symbols with no declarations 258 while (finalSymbol && (!finalSymbol.declarations || finalSymbol.declarations.length === 0) && 259 (finalSymbol.flags & ts.SymbolFlags.Alias)) { 260 if (visited.has(finalSymbol)) { 261 break; 262 } 263 visited.add(finalSymbol); 264 const aliased = checker.getAliasedSymbol(finalSymbol); 265 if (!aliased || aliased === finalSymbol) { 266 break; 267 } 268 finalSymbol = aliased; 269 } 270 271 return finalSymbol; 272} 273 274function getDefaultExportName(symbol: ts.Symbol): [boolean, string] { 275 const decl = symbol.valueDeclaration ?? symbol.declarations?.[0]; 276 if (!decl) { 277 return [false, '']; 278 } 279 if (ts.isVariableDeclaration(decl)) { 280 const parent = decl.parent?.parent; 281 if (ts.isVariableStatement(parent) && parent.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) { 282 return [false, symbol.name]; 283 } 284 } 285 const sourceFile = decl.getSourceFile(); 286 for (const stmt of sourceFile.statements) { 287 const result: [boolean, string] | undefined = checkExportAssignment(stmt, symbol.name) ?? 288 checkExportDeclaration(stmt, symbol.name) ?? checkNamedExportDeclaration(stmt, decl); 289 if (result !== undefined) { 290 return result; 291 } 292 } 293 return [false, '']; 294} 295 296function checkExportAssignment(stmt: ts.Statement, symbolName: string): [boolean, string] | undefined { 297 if (ts.isExportAssignment(stmt) && !stmt.isExportEquals && ts.isIdentifier(stmt.expression) && stmt.expression.text === symbolName) { 298 return [true, 'default']; 299 } 300 return undefined; 301} 302 303function checkExportDeclaration(stmt: ts.Statement, symbolName: string): [boolean, string] | undefined { 304 if (!ts.isExportDeclaration(stmt) || !stmt.exportClause || !ts.isNamedExports(stmt.exportClause)) { 305 return undefined; 306 } 307 for (const specifier of stmt.exportClause.elements) { 308 if (specifier.name.text === 'default' && specifier.propertyName?.text === symbolName) { 309 return [true, 'default']; 310 } 311 if (specifier.name.text === 'default' && !specifier.propertyName) { 312 return [false, '']; 313 } 314 if (specifier.name.text !== 'default' && specifier.propertyName?.text === symbolName) { 315 return [false, specifier.name.text]; 316 } 317 if (specifier.name.text !== 'default' && specifier.name.text === symbolName) { 318 return [false, symbolName]; 319 } 320 } 321 return undefined; 322} 323 324function checkNamedExportDeclaration(stmt: ts.Statement, decl: ts.Declaration): [boolean, string] | undefined { 325 if (stmt.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) && stmt.name?.text === decl.name?.getText()) { 326 if (stmt.modifiers?.some(m => m.kind === ts.SyntaxKind.DefaultKeyword)) { 327 return [true, 'default']; 328 } 329 return [false, stmt.name?.text]; 330 } 331 return undefined; 332}