• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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