• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}