• 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 ts from 'typescript';
17
18import { EXTNAME_D_ETS } from './pre_define';
19
20import {
21  whiteList,
22  decoratorsWhiteList,
23} from './import_whiteList';
24
25const fs = require('fs');
26const path = require('path');
27
28function getDeclgenFiles(dir: string, filePaths: string[] = []) {
29  const files = fs.readdirSync(dir);
30
31  files.forEach(file => {
32    const filePath = path.join(dir, file);
33    const stat = fs.statSync(filePath);
34
35    if (stat.isDirectory()) {
36      getDeclgenFiles(filePath, filePaths);
37    } else if (stat.isFile() && file.endsWith(EXTNAME_D_ETS)) {
38      filePaths.push(filePath);
39    }
40  });
41
42  return filePaths;
43}
44
45export function isStructDeclaration(node: ts.Node): boolean {
46  return ts.isStructDeclaration(node);
47}
48
49function defaultCompilerOptions(): ts.CompilerOptions {
50  return {
51    target: ts.ScriptTarget.Latest,
52    module: ts.ModuleKind.CommonJS,
53    allowJs: true,
54    checkJs: true,
55    declaration: true,
56    emitDeclarationOnly: true,
57    noEmit: false
58  };
59}
60
61function getSourceFiles(program: ts.Program, filePaths: string[]): ts.SourceFile[] {
62  const sourceFiles: ts.SourceFile[] = [];
63
64  filePaths.forEach(filePath => {
65    sourceFiles.push(program.getSourceFile(filePath));
66  });
67
68  return sourceFiles;
69}
70
71class HandleUIImports {
72  private context: ts.TransformationContext;
73  private typeChecker: ts.TypeChecker;
74
75  private readonly outPath: string;
76
77  private importedInterfaces: Set<string> = new Set<string>();
78  private interfacesNeedToImport: Set<string> = new Set<string>();
79  private printer = ts.createPrinter();
80  private insertPosition = 0;
81
82  private readonly trueSymbolAtLocationCache = new Map<ts.Node, ts.Symbol | null>();
83
84  constructor(program: ts.Program, context: ts.TransformationContext, outPath: string) {
85    this.context = context;
86    this.typeChecker = program.getTypeChecker();
87    this.outPath = outPath;
88  }
89
90  public createCustomTransformer(sourceFile: ts.SourceFile) {
91    this.extractImportedNames(sourceFile);
92
93    const statements = sourceFile.statements;
94    for (let i = 0; i < statements.length; ++i) {
95      const statement = statements[i];
96      if (!ts.isJSDoc(statement) && !(ts.isExpressionStatement(statement) &&
97        ts.isStringLiteral(statement.expression))) {
98          this.insertPosition = i;
99          break;
100        }
101    }
102
103    return ts.visitNode(sourceFile, this.visitNode.bind(this))
104  }
105
106  private visitNode(node: ts.Node): ts.Node | undefined {
107    // delete constructor
108    if (node.parent && isStructDeclaration(node.parent) && ts.isConstructorDeclaration(node)) {
109      return;
110    }
111
112    // skip to collect origin import from 1.2
113    if (ts.isImportDeclaration(node)) {
114      const moduleSpecifier = node.moduleSpecifier;
115      if (ts.isStringLiteral(moduleSpecifier)) {
116        const modulePath = moduleSpecifier.text;
117        if (['@ohos.arkui.stateManagement', '@ohos.arkui.component'].includes(modulePath)) {
118          return node;
119        }
120      }
121    }
122
123    this.handleImportBuilder(node);
124    const result = ts.visitEachChild(node, this.visitNode.bind(this), this.context);
125
126    if (ts.isIdentifier(result) && !this.shouldSkipIdentifier(result)) {
127      this.interfacesNeedToImport.add(result.text);
128    } else if (ts.isSourceFile(result)) {
129      this.AddUIImports(result);
130    }
131
132    return result;
133  }
134
135  private handleImportBuilder(node: ts.Node): void {
136    ts.getAllDecorators(node)?.forEach(element => {
137      if (element?.getText() === '@Builder') {
138        this.interfacesNeedToImport.add('Builder');
139        return;
140      }
141    });
142  }
143
144  private AddInteropImports(): ts.ImportDeclaration {
145    const moduleName = 'arkui.component.interop';
146    const interopImportName = [
147      'compatibleComponent',
148      'bindCompatibleProvideCallback',
149      'getCompatibleState'
150    ];
151    const interopImportSpecifiers: ts.ImportSpecifier[] = [];
152    interopImportName.forEach((interopName) => {
153      const identifier = ts.factory.createIdentifier(interopName);
154      const specifier = ts.factory.createImportSpecifier(false, undefined, identifier);
155      interopImportSpecifiers.push(specifier);
156    });
157    const compImportDeclaration = ts.factory.createImportDeclaration(
158      undefined,
159      ts.factory.createImportClause(false,
160        undefined,
161        ts.factory.createNamedImports(
162          interopImportSpecifiers
163        )
164      ),
165      ts.factory.createStringLiteral(moduleName, true),
166      undefined
167    );
168    return compImportDeclaration;
169  }
170
171  private AddUIImports(node: ts.SourceFile): void {
172    const compImportSpecifiers: ts.ImportSpecifier[] = [];
173    const stateImportSpecifiers: ts.ImportSpecifier[] = [];
174
175    this.interfacesNeedToImport.forEach((interfaceName) => {
176      if (this.importedInterfaces.has(interfaceName)) {
177        return;
178      }
179      const identifier = ts.factory.createIdentifier(interfaceName);
180      if (decoratorsWhiteList.includes(interfaceName)) {
181        stateImportSpecifiers.push(ts.factory.createImportSpecifier(false, undefined, identifier));
182      } else {
183        compImportSpecifiers.push(ts.factory.createImportSpecifier(false, undefined, identifier));
184      }
185    });
186
187    if (compImportSpecifiers.length + stateImportSpecifiers.length > 0) {
188      const newStatements = [...node.statements];
189
190      if (compImportSpecifiers.length) {
191        const moduleName = '@ohos.arkui.component';
192        const compImportDeclaration = ts.factory.createImportDeclaration(
193          undefined,
194          ts.factory.createImportClause(false,
195            undefined,
196            ts.factory.createNamedImports(
197              compImportSpecifiers
198            )
199          ),
200          ts.factory.createStringLiteral(moduleName, true),
201          undefined
202        );
203        newStatements.splice(this.insertPosition, 0, compImportDeclaration);
204      }
205
206      if (stateImportSpecifiers.length) {
207        const moduleName = '@ohos.arkui.stateManagement';
208        const stateImportDeclaration = ts.factory.createImportDeclaration(
209          undefined,
210          ts.factory.createImportClause(false,
211            undefined,
212            ts.factory.createNamedImports(
213              stateImportSpecifiers
214            )
215          ),
216          ts.factory.createStringLiteral(moduleName, true),
217          undefined
218        );
219        newStatements.splice(this.insertPosition, 0, stateImportDeclaration);
220      }
221
222      newStatements.splice(this.insertPosition, 0, this.AddInteropImports());
223
224      const updatedStatements = ts.factory.createNodeArray(newStatements);
225      const updatedSourceFile = ts.factory.updateSourceFile(node,
226        updatedStatements,
227        node.isDeclarationFile,
228        node.referencedFiles,
229        node.typeReferenceDirectives,
230        node.hasNoDefaultLib,
231        node.libReferenceDirectives
232      );
233
234      const updatedCode = this.printer.printFile(updatedSourceFile);
235      if (this.outPath) {
236        fs.writeFileSync(this.outPath, updatedCode);
237      } else {
238        fs.writeFileSync(updatedSourceFile.fileName, updatedCode);
239      }
240    }
241  }
242
243  private getDeclarationNode(node: ts.Node): ts.Declaration | undefined {
244    const symbol = this.trueSymbolAtLocation(node);
245    return HandleUIImports.getDeclaration(symbol);
246  }
247
248  static getDeclaration(tsSymbol: ts.Symbol | undefined): ts.Declaration | undefined {
249    if (tsSymbol?.declarations && tsSymbol.declarations.length > 0) {
250      return tsSymbol.declarations[0];
251    }
252
253    return undefined;
254  }
255
256  private followIfAliased(symbol: ts.Symbol): ts.Symbol {
257    if ((symbol.getFlags() & ts.SymbolFlags.Alias) !== 0) {
258      return this.typeChecker.getAliasedSymbol(symbol);
259    }
260
261    return symbol;
262  }
263
264  private trueSymbolAtLocation(node: ts.Node): ts.Symbol | undefined {
265    const cache = this.trueSymbolAtLocationCache;
266    const val = cache.get(node);
267
268    if (val !== undefined) {
269      return val !== null ? val : undefined;
270    }
271
272    let symbol = this.typeChecker.getSymbolAtLocation(node);
273
274    if (symbol === undefined) {
275      cache.set(node, null);
276      return undefined;
277    }
278
279    symbol = this.followIfAliased(symbol);
280    cache.set(node, symbol);
281
282    return symbol;
283  }
284
285  private shouldSkipIdentifier(identifier: ts.Identifier): boolean {
286    const name = identifier.text;
287    const skippedList = new Set<string>(['Extend', 'Styles']);
288
289    if (skippedList.has(name)) {
290      return true;
291    }
292
293    if (!whiteList.has(name)) {
294      return true;
295    }
296
297    const symbol = this.typeChecker.getSymbolAtLocation(identifier);
298    if (symbol) {
299      const decl = this.getDeclarationNode(identifier);
300      if (decl?.getSourceFile() === identifier.getSourceFile()) {
301        return true;
302      }
303    }
304
305    if (this.interfacesNeedToImport.has(name)) {
306      return true;
307    }
308
309    return false;
310  }
311
312  private extractImportedNames(sourceFile: ts.SourceFile): void {
313    for (const statement of sourceFile.statements) {
314      if (!ts.isImportDeclaration(statement)) {
315        continue;
316      }
317
318      const importClause = statement.importClause;
319      if (!importClause) {
320        continue;
321      }
322
323      const namedBindings = importClause.namedBindings;
324      if (!namedBindings || !ts.isNamedImports(namedBindings)) {
325        continue;
326      }
327
328      for (const specifier of namedBindings.elements) {
329        const importedName = specifier.name.getText(sourceFile);
330        this.importedInterfaces.add(importedName);
331      }
332    }
333  }
334}
335
336/**
337 * process interop ui
338 *
339 * @param path - declgenV2OutPath
340 */
341export function processInteropUI(path: string, outPath = ''): void {
342  const filePaths = getDeclgenFiles(path);
343  const program = ts.createProgram(filePaths, defaultCompilerOptions());
344  const sourceFiles = getSourceFiles(program, filePaths);
345
346  const createTransformer = (ctx: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
347    return (sourceFile: ts.SourceFile) => {
348      const handleUIImports = new HandleUIImports(program, ctx, outPath);
349      return handleUIImports.createCustomTransformer(sourceFile);
350    }
351  }
352  ts.transform(sourceFiles, [createTransformer]);
353}
354