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