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 path from 'path'; 17import * as fs from 'fs'; 18 19import { 20 Logger, 21 LogData, 22 LogDataFactory 23} from '../logger'; 24import { 25 ErrorCode 26} from '../error_code'; 27import { 28 changeFileExtension, 29 ensurePathExists, 30 isSubPathOf, 31 toUnixPath 32} from '../utils'; 33import { 34 BuildConfig, 35 ModuleInfo, 36 PathsConfig 37} from '../types'; 38import { 39 LANGUAGE_VERSION, 40 SYSTEM_SDK_PATH_FROM_SDK, 41} from '../pre_define'; 42 43interface DynamicPathItem { 44 language: string, 45 declPath: string, 46 ohmUrl: string 47} 48 49interface ArkTSConfigObject { 50 compilerOptions: { 51 package: string, 52 baseUrl: string, 53 paths: Record<string, string[]>; 54 dependencies: string[] | undefined; 55 entry?: string; 56 dynamicPaths: Record<string, DynamicPathItem>; 57 useEmptyPackage?: boolean; 58 } 59}; 60 61export class ArkTSConfigGenerator { 62 private static instance: ArkTSConfigGenerator | undefined; 63 private stdlibStdPath: string; 64 private stdlibEscompatPath: string; 65 private systemSdkPath: string; 66 private externalApiPaths: string[]; 67 68 private moduleInfos: Map<string, ModuleInfo>; 69 private pathSection: Record<string, string[]>; 70 71 private logger: Logger; 72 73 private constructor(buildConfig: BuildConfig, moduleInfos: Map<string, ModuleInfo>) { 74 let pandaStdlibPath: string = buildConfig.pandaStdlibPath ?? 75 path.resolve(buildConfig.pandaSdkPath!!, 'lib', 'stdlib'); 76 this.stdlibStdPath = path.resolve(pandaStdlibPath, 'std'); 77 this.stdlibEscompatPath = path.resolve(pandaStdlibPath, 'escompat'); 78 this.systemSdkPath = path.resolve(buildConfig.buildSdkPath, SYSTEM_SDK_PATH_FROM_SDK); 79 this.externalApiPaths = buildConfig.externalApiPaths; 80 81 this.moduleInfos = moduleInfos; 82 this.pathSection = {}; 83 84 this.logger = Logger.getInstance(); 85 } 86 87 public static getInstance(buildConfig?: BuildConfig, moduleInfos?: Map<string, ModuleInfo>): ArkTSConfigGenerator { 88 if (!ArkTSConfigGenerator.instance) { 89 if (!buildConfig || !moduleInfos) { 90 throw new Error( 91 'buildConfig and moduleInfos is required for the first instantiation of ArkTSConfigGenerator.'); 92 } 93 ArkTSConfigGenerator.instance = new ArkTSConfigGenerator(buildConfig, moduleInfos); 94 } 95 return ArkTSConfigGenerator.instance; 96 } 97 98 public static destroyInstance(): void { 99 ArkTSConfigGenerator.instance = undefined; 100 } 101 102 private generateSystemSdkPathSection(pathSection: Record<string, string[]>): void { 103 function traverse(currentDir: string, relativePath: string = '', isExcludedDir: boolean = false, allowedExtensions: string[] = ['.d.ets']): void { 104 const items = fs.readdirSync(currentDir); 105 for (const item of items) { 106 const itemPath = path.join(currentDir, item); 107 const stat = fs.statSync(itemPath); 108 const isAllowedFile = allowedExtensions.some(ext => item.endsWith(ext)); 109 if (stat.isFile() && !isAllowedFile) { 110 continue; 111 } 112 113 if (stat.isFile()) { 114 const basename = path.basename(item, '.d.ets'); 115 const key = isExcludedDir ? basename : (relativePath ? `${relativePath}.${basename}` : basename); 116 pathSection[key] = [changeFileExtension(itemPath, '', '.d.ets')]; 117 } 118 if (stat.isDirectory()) { 119 // For files under api dir excluding arkui/runtime-api dir, 120 // fill path section with `"pathFromApi.subdir.fileName" = [${absolute_path_to_file}]`; 121 // For @koalaui files under arkui/runtime-api dir, 122 // fill path section with `"fileName" = [${absolute_path_to_file}]`. 123 const isCurrentDirExcluded = path.basename(currentDir) === 'arkui' && item === 'runtime-api'; 124 const newRelativePath = isCurrentDirExcluded ? '' : (relativePath ? `${relativePath}.${item}` : item); 125 traverse(path.resolve(currentDir, item), newRelativePath, isCurrentDirExcluded || isExcludedDir); 126 } 127 } 128 } 129 130 if (this.externalApiPaths && this.externalApiPaths.length !== 0) { 131 this.externalApiPaths.forEach((sdkPath: string) => { 132 fs.existsSync(sdkPath) ? traverse(sdkPath) : this.logger.printWarn(`sdk path ${sdkPath} not exist.`); 133 }); 134 } else { 135 // Search openharmony sdk only, we keep them for ci compatibility. 136 let apiPath: string = path.resolve(this.systemSdkPath, 'api'); 137 fs.existsSync(apiPath) ? traverse(apiPath) : this.logger.printWarn(`sdk path ${apiPath} not exist.`); 138 139 let arktsPath: string = path.resolve(this.systemSdkPath, 'arkts'); 140 fs.existsSync(arktsPath) ? traverse(arktsPath) : this.logger.printWarn(`sdk path ${arktsPath} not exist.`); 141 142 let kitsPath: string = path.resolve(this.systemSdkPath, 'kits'); 143 fs.existsSync(kitsPath) ? traverse(kitsPath) : this.logger.printWarn(`sdk path ${kitsPath} not exist.`); 144 } 145 } 146 147 private getPathSection(): Record<string, string[]> { 148 if (Object.keys(this.pathSection).length !== 0) { 149 return this.pathSection; 150 } 151 152 this.pathSection.std = [this.stdlibStdPath]; 153 this.pathSection.escompat = [this.stdlibEscompatPath]; 154 155 this.generateSystemSdkPathSection(this.pathSection); 156 157 this.moduleInfos.forEach((moduleInfo: ModuleInfo, packageName: string) => { 158 if (moduleInfo.language !== LANGUAGE_VERSION.ARKTS_1_2 && moduleInfo.language !== LANGUAGE_VERSION.ARKTS_HYBRID) { 159 return; 160 } 161 if (!moduleInfo.entryFile) { 162 return; 163 } 164 this.handleEntryFile(moduleInfo); 165 }); 166 return this.pathSection; 167 } 168 169 private handleEntryFile(moduleInfo: ModuleInfo): void { 170 try { 171 const stat = fs.statSync(moduleInfo.entryFile); 172 if (!stat.isFile()) { 173 return; 174 } 175 const entryFilePath = moduleInfo.entryFile; 176 const firstLine = fs.readFileSync(entryFilePath, 'utf-8').split('\n')[0]; 177 // If the file is an ArkTS 1.2 implementation, configure the path in pathSection. 178 if (moduleInfo.language === LANGUAGE_VERSION.ARKTS_1_2 || moduleInfo.language === LANGUAGE_VERSION.ARKTS_HYBRID && firstLine.includes('use static')) { 179 this.pathSection[moduleInfo.packageName] = [ 180 path.resolve(moduleInfo.moduleRootPath, moduleInfo.sourceRoots[0]) 181 ]; 182 } 183 } catch (error) { 184 const logData: LogData = LogDataFactory.newInstance( 185 ErrorCode.BUILDSYSTEM_HANDLE_ENTRY_FILE, 186 `Error handle entry file for module ${moduleInfo.packageName}` 187 ); 188 this.logger.printError(logData); 189 } 190 } 191 192 private getDependenciesSection(moduleInfo: ModuleInfo, dependenciesSection: string[]): void { 193 let depModules: Map<string, ModuleInfo> = moduleInfo.staticDepModuleInfos; 194 depModules.forEach((depModuleInfo: ModuleInfo) => { 195 dependenciesSection.push(depModuleInfo.arktsConfigFile); 196 }); 197 } 198 199 private getOhmurl(file: string, moduleInfo: ModuleInfo): string { 200 let unixFilePath: string = file.replace(/\\/g, '/'); 201 let ohmurl: string = moduleInfo.packageName + '/' + unixFilePath; 202 return changeFileExtension(ohmurl, ''); 203 } 204 205 private getDynamicPathSection(moduleInfo: ModuleInfo, dynamicPathSection: Record<string, DynamicPathItem>): void { 206 let depModules: Map<string, ModuleInfo> = moduleInfo.dynamicDepModuleInfos; 207 208 depModules.forEach((depModuleInfo: ModuleInfo) => { 209 if (!depModuleInfo.declFilesPath || !fs.existsSync(depModuleInfo.declFilesPath)) { 210 console.error(`Module ${moduleInfo.packageName} depends on dynamic module ${depModuleInfo.packageName}, but 211 decl file not found on path ${depModuleInfo.declFilesPath}`); 212 return; 213 } 214 let declFilesObject = JSON.parse(fs.readFileSync(depModuleInfo.declFilesPath, 'utf-8')); 215 Object.keys(declFilesObject.files).forEach((file: string)=> { 216 let ohmurl: string = this.getOhmurl(file, depModuleInfo); 217 dynamicPathSection[ohmurl] = { 218 language: 'js', 219 declPath: declFilesObject.files[file].declPath, 220 ohmUrl: declFilesObject.files[file].ohmUrl 221 }; 222 223 let absFilePath: string = path.resolve(depModuleInfo.moduleRootPath, file); 224 let entryFileWithoutExtension: string = changeFileExtension(depModuleInfo.entryFile, ''); 225 if (absFilePath === entryFileWithoutExtension) { 226 dynamicPathSection[depModuleInfo.packageName] = dynamicPathSection[ohmurl]; 227 } 228 }); 229 }); 230 } 231 232 public writeArkTSConfigFile( 233 moduleInfo: ModuleInfo, 234 enableDeclgenEts2Ts: boolean, 235 buildConfig: BuildConfig 236 ): void { 237 if (!moduleInfo.sourceRoots || moduleInfo.sourceRoots.length === 0) { 238 const logData: LogData = LogDataFactory.newInstance( 239 ErrorCode.BUILDSYSTEM_SOURCEROOTS_NOT_SET_FAIL, 240 'SourceRoots not set from hvigor.' 241 ); 242 this.logger.printErrorAndExit(logData); 243 } 244 let pathSection = this.getPathSection(); 245 let dependenciesSection: string[] = []; 246 this.getDependenciesSection(moduleInfo, dependenciesSection); 247 this.getAllFilesToPathSectionForHybrid(moduleInfo, buildConfig); 248 249 let dynamicPathSection: Record<string, DynamicPathItem> = {}; 250 251 if (!enableDeclgenEts2Ts) { 252 this.getDynamicPathSection(moduleInfo, dynamicPathSection); 253 } 254 255 let baseUrl: string = path.resolve(moduleInfo.moduleRootPath, moduleInfo.sourceRoots[0]); 256 if (buildConfig.paths) { 257 Object.entries(buildConfig.paths).map(([key, value]) => { 258 pathSection[key] = value 259 }); 260 } 261 let arktsConfig: ArkTSConfigObject = { 262 compilerOptions: { 263 package: moduleInfo.packageName, 264 baseUrl: baseUrl, 265 paths: pathSection, 266 dependencies: dependenciesSection.length === 0 ? undefined : dependenciesSection, 267 entry: moduleInfo.entryFile, 268 dynamicPaths: dynamicPathSection 269 } 270 }; 271 272 if (moduleInfo.entryFile && moduleInfo.language === LANGUAGE_VERSION.ARKTS_HYBRID) { 273 const entryFilePath = moduleInfo.entryFile; 274 const stat = fs.statSync(entryFilePath); 275 if (fs.existsSync(entryFilePath) && stat.isFile()) { 276 const firstLine = fs.readFileSync(entryFilePath, 'utf-8').split('\n')[0]; 277 // If the entryFile is not an ArkTS 1.2 implementation, remove the entry property field. 278 if (!firstLine.includes('use static')) { 279 delete arktsConfig.compilerOptions.entry; 280 } 281 } 282 } 283 284 if (moduleInfo.frameworkMode) { 285 arktsConfig.compilerOptions.useEmptyPackage = moduleInfo.useEmptyPackage; 286 } 287 288 ensurePathExists(moduleInfo.arktsConfigFile); 289 fs.writeFileSync(moduleInfo.arktsConfigFile, JSON.stringify(arktsConfig, null, 2), 'utf-8'); 290 } 291 292 public getAllFilesToPathSectionForHybrid( 293 moduleInfo: ModuleInfo, 294 buildConfig: BuildConfig 295 ): void { 296 if (moduleInfo?.language !== LANGUAGE_VERSION.ARKTS_HYBRID) { 297 return; 298 } 299 300 const projectRoot = toUnixPath(buildConfig.projectRootPath) + '/'; 301 const moduleRoot = toUnixPath(moduleInfo.moduleRootPath); 302 303 for (const file of buildConfig.compileFiles) { 304 const unixFilePath = toUnixPath(file); 305 306 if (!isSubPathOf(unixFilePath, moduleRoot)) { 307 continue; 308 } 309 310 let relativePath = unixFilePath.startsWith(projectRoot) 311 ? unixFilePath.substring(projectRoot.length) 312 : unixFilePath; 313 const keyWithoutExtension = relativePath.replace(/\.[^/.]+$/, ''); 314 this.pathSection[keyWithoutExtension] = [file]; 315 } 316 } 317} 318