1/* 2 * Copyright (c) 2023 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 16const JSON5 = require('json5'); 17const path = require('path'); 18const fs = require('fs'); 19 20class Project { 21 constructor(projectPath, nonProject) { 22 this.projectPath = projectPath; 23 this.nonProject = nonProject; 24 this.logTag = 'Project'; 25 } 26 27 getPath() { 28 return this.projectPath; 29 } 30 31 getProfile() { 32 if (!this.profile) { 33 const buildProfilePath = path.resolve(this.projectPath, 'build-profile.json5'); 34 if (!fs.existsSync(buildProfilePath)) { 35 Logger.error(this.logTag, 'build-profile.json5 can\'t be found, is it an openharmony project?'); 36 return this.profile; 37 } 38 const profileContent = fs.readFileSync(buildProfilePath, 'utf-8'); 39 try { 40 this.profile = JSON5.parse(profileContent); 41 } catch (ex) { 42 Logger.error(this.logTag, `parse build-profile.json error: ${JSON.stringify(ex)}`); 43 } 44 } 45 return this.profile; 46 } 47 48 getAppSdkVersion() { 49 const profile = this.getProfile(); 50 51 if (!profile) { 52 return undefined; 53 } 54 55 if (!profile.app) { 56 return undefined; 57 } 58 59 if (profile.app.compileSdkVersion) { 60 return profile.app.compileSdkVersion; 61 } 62 63 if (profile.app.products) { 64 const compileSdkVersion = profile.app.products[0].compileSdkVersion; 65 if (typeof compileSdkVersion === 'number') { 66 return compileSdkVersion; 67 } 68 const version = compileSdkVersion.match(/\((.+)\)/g)[0].replace(/\(|\)/g, ''); 69 return version; 70 } 71 return undefined; 72 } 73 74 getAppSdkPath() { 75 if (this.sdkPath) { 76 return this.sdkPath; 77 } 78 const localPropertiesPath = path.resolve(this.projectPath, 'local.properties'); 79 if (!fs.existsSync(localPropertiesPath)) { 80 Logger.error(this.logTag, 'unable to get the sdk path of the project, specify it using the --sdk or --sdkRoot'); 81 return this.sdkPath; 82 } 83 const properties = this.parseProperty(localPropertiesPath); 84 this.sdkPath = properties.get('sdk.dir'); 85 return this.sdkPath; 86 } 87 88 parseProperty(propertyFilePath) { 89 const properties = fs.readFileSync(propertyFilePath, 'utf-8'); 90 const lines = properties.split('\n'); 91 const propertyRegExp = new RegExp(/(.*)=(.*)/); 92 const map = new Map(); 93 lines.forEach((line) => { 94 if (line.startsWith('#')) { 95 return; 96 } 97 const expArray = line.match(propertyRegExp); 98 const MATCHED_RESULT_NUMBER = 3; 99 const KEY_INDEX = 1; 100 const VALUE_INDEX = 2; 101 if (expArray && expArray.length === MATCHED_RESULT_NUMBER) { 102 map.set(expArray[KEY_INDEX].trim(), expArray[VALUE_INDEX].trim()); 103 } 104 }); 105 return map; 106 } 107 108 /** 109 * 获取应用的源码列表 110 * 111 * @returns 112 */ 113 getAppSources(isIncludeTest) { 114 if (this.nonProject) { 115 return this.getNonProjectAppSources(); 116 } 117 const profile = this.getProfile(); 118 if (!profile || !profile.modules || profile.modules.length === 0) { 119 return new Set(); 120 } 121 const moduleSrcPaths = []; 122 profile.modules.forEach((module) => { 123 if (module.srcPath) { 124 moduleSrcPaths.push(path.resolve(this.projectPath, module.srcPath)); 125 } 126 }); 127 const appSources = []; 128 moduleSrcPaths.forEach((moduleSrc) => { 129 appSources.push(...this.getModuleSource(moduleSrc, isIncludeTest)); 130 }); 131 return new Set(appSources); 132 } 133 134 getNonProjectAppSources() { 135 Logger.info(this.logTag, 'find source files in non-project'); 136 const appSources = []; 137 this.listSourceFiles(this.projectPath, appSources); 138 return new Set(appSources); 139 } 140 141 getModuleSource(modulePath, isIncludeTest) { 142 const sourceSets = ['src/main/ets']; 143 if (isIncludeTest) { 144 sourceSets.push(...['src/ohosTest/ets']); 145 } 146 const sources = []; 147 sourceSets.forEach((sourcePath) => { 148 const srcPath = path.resolve(modulePath, sourcePath); 149 this.listSourceFiles(srcPath, sources); 150 }); 151 if (sources.length === 0) { 152 Logger.info(this.logTag, `can't find source file in ${this.projectPath}`); 153 } 154 return sources; 155 } 156 157 listSourceFiles(srcPath, dest) { 158 if (fs.existsSync(srcPath)) { 159 Logger.info(this.logTag, `find source code in ${srcPath}`); 160 FileSystem.listFiles(srcPath, (filePath) => { 161 const fileName = path.basename(filePath); 162 return fileName.endsWith('.ts') || fileName.endsWith('.ets'); 163 }, dest); 164 } 165 } 166} 167 168class Sdk { 169 170 /** 171 * 172 * @param {Project} project 应用工程对象 173 * @param {string} sdkEtsPath 指向sdk中ets目录的路径 174 * @param {string} sdkRoot sdk根目录 175 */ 176 constructor(project, sdkEtsPath, sdkRoot) { 177 this.project = project; 178 this.sdkEtsPath = sdkEtsPath; 179 this.sdkRoot = sdkRoot; 180 } 181 182 getPath() { 183 if (this.sdkEtsPath) { 184 return this.sdkEtsPath; 185 } 186 if (this.sdkApiRoot) { 187 return this.sdkApiRoot; 188 } 189 const sdkVersion = this.project.getAppSdkVersion(); 190 const sdkDir = this.sdkRoot || this.project.getAppSdkPath(); 191 if (sdkVersion && sdkDir) { 192 this.sdkApiRoot = path.resolve(sdkDir, `${sdkVersion}`, 'ets'); 193 } 194 return this.sdkApiRoot; 195 } 196 197 /** 198 * 获取SDK的d.ts文件列表 199 * 200 * @param {string} sdkRoot 201 * @returns 202 */ 203 getApiLibs() { 204 if (this.apiLibs) { 205 return this.apiLibs; 206 } 207 this.apiLibs = []; 208 this.listDtsFiles('api', this.apiLibs); 209 return this.apiLibs; 210 } 211 212 getComponentLibs() { 213 if (this.componentLibs) { 214 return this.componentLibs; 215 } 216 this.componentLibs = []; 217 this.listDtsFiles('component', this.componentLibs); 218 return this.componentLibs; 219 } 220 221 getESLibs(libPath) { 222 if (!process.env.bundleMode) { 223 return []; 224 } 225 Logger.info('Sdk', `find ES libs in ${libPath}`); 226 if (this.esLibs) { 227 return this.esLibs; 228 } 229 this.esLibs = []; 230 FileSystem.listFiles(libPath, (filePath) => path.basename(filePath).endsWith('.d.ts'), this.esLibs); 231 FileSystem.listFiles(libPath, (filePath) => path.basename(filePath).endsWith('.d.ets'), this.esLibs); 232 return this.esLibs; 233 } 234 235 listDtsFiles(dir, dest) { 236 const sdkRoot = this.getPath(); 237 if (!sdkRoot) { 238 return; 239 } 240 const subDir = path.resolve(sdkRoot, dir); 241 FileSystem.listFiles(subDir, (filePath) => path.basename(filePath).endsWith('.d.ts'), dest); 242 FileSystem.listFiles(subDir, (filePath) => path.basename(filePath).endsWith('.d.ets'), dest); 243 } 244} 245 246class FileSystem { 247 static listFiles(dir, filter, dest) { 248 const files = fs.readdirSync(dir); 249 files.forEach((element) => { 250 const filePath = path.join(dir, element); 251 const status = fs.statSync(filePath); 252 if (status.isDirectory()) { 253 this.listFiles(filePath, filter, dest); 254 } else if (filter(filePath)) { 255 dest.push(this.convertToPosixPath(filePath)); 256 } 257 }); 258 } 259 260 static convertToPosixPath(filePath) { 261 return filePath.split(path.sep).join(path.posix.sep); 262 } 263 264 static isInDirectory(parentDir, subPath) { 265 const relative = path.relative(parentDir, subPath); 266 return (relative === '' || !relative.startsWith('..')) && !path.isAbsolute(relative); 267 } 268 269 static listAllAppDirs(parentDir) { 270 const dest = []; 271 this.listDirectory(parentDir, dest, (filePath) => { 272 const buildProfilePath = path.resolve(filePath, 'build-profile.json5'); 273 if (!fs.existsSync(buildProfilePath)) { 274 return false; 275 } 276 const profileContent = fs.readFileSync(buildProfilePath, 'utf-8'); 277 const profile = JSON5.parse(profileContent); 278 return profile.app && profile.modules; 279 }, (filePath) => { 280 return filePath; 281 }); 282 return dest; 283 } 284 285 static listDirectory(dir, dest, filter, visitChildren) { 286 const files = fs.readdirSync(dir); 287 files.forEach((element) => { 288 const filePath = path.join(dir, element); 289 const status = fs.statSync(filePath); 290 if (status.isDirectory()) { 291 if (filter(filePath)) { 292 dest.push(filePath); 293 } else if (visitChildren(filePath)) { 294 this.listDirectory(filePath, dest, filter, visitChildren); 295 } 296 } 297 }); 298 } 299} 300 301class Logger { 302 static INFO = 0; 303 static WARN = 1; 304 static ERROR = 2; 305 static logs = ''; 306 static LEVEL_NAME = new Map([ 307 [this.INFO, 'I'], 308 [this.WARN, 'W'], 309 [this.ERROR, 'E'] 310 ]); 311 312 static info(tag, message) { 313 this.wrap(this.INFO, tag, message); 314 } 315 316 static warn(tag, message) { 317 this.wrap(this.WARN, tag, message); 318 } 319 320 static error(tag, message) { 321 this.wrap(this.ERROR, tag, message); 322 } 323 324 static wrap(level, tag, message) { 325 const timeStamp = `${this.formatDate(Date.now(), 'Y-M-D H:m:s:x')}`; 326 const logMessage = `${timeStamp} ${this.getLevelName(level)} [${tag}] ${message}`; 327 console.log(logMessage); 328 } 329 330 static flush(output) { 331 const logName = path.resolve(output, `${this.formatDate(Date.now(), 'Y-M-D-Hmsx')}.log`); 332 fs.writeFileSync(logName, this.logs); 333 this.info('Logger', `log is in ${logName}`); 334 } 335 336 static getLevelName(level) { 337 if (this.LEVEL_NAME.has(level)) { 338 return this.LEVEL_NAME.get(level); 339 } 340 return this.LEVEL_NAME.get(this.INFO); 341 } 342 343 static formatDate(time, format) { 344 const date = new Date(time); 345 const year = date.getFullYear(); 346 const month = date.getMonth() + 1; 347 const day = date.getDate(); 348 const hour = date.getHours(); 349 const min = date.getMinutes(); 350 const sec = date.getSeconds(); 351 const mis = date.getMilliseconds(); 352 let dateStr = format.replace('Y', `${year}`); 353 dateStr = dateStr.replace('M', `${month}`); 354 dateStr = dateStr.replace('D', `${day}`); 355 dateStr = dateStr.replace('H', `${hour}`); 356 dateStr = dateStr.replace('m', `${min}`); 357 dateStr = dateStr.replace('s', `${sec}`); 358 dateStr = dateStr.replace('x', `${mis}`); 359 return dateStr; 360 } 361} 362 363exports.Project = Project; 364exports.Sdk = Sdk; 365exports.FileSystem = FileSystem; 366exports.Logger = Logger;