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;