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