* Copyright (c) 2025-2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as path from 'path';
import * as fs from 'fs';
import * as JSON5 from 'json5';
import { changeFileExtension, ensurePathExists, getFileLanguageVersion } from './utils';
import { AliasConfig, BuildConfig, ModuleInfo } from './types';
import { EXTERNAL_API_PATH_FROM_SDK, LANGUAGE_VERSION, PANDA_SDK_PATH_FROM_SDK, SYSTEM_SDK_PATH_FROM_SDK } from './preDefine';
import { logger } from '../lsp/logger';
interface DependencyItem {
language: string;
path: string;
ohmUrl?: string;
alias?: string[];
}
interface ArkTSConfigObject {
compilerOptions: {
package: string;
baseUrl: string;
paths: Record<string, string[]>;
entry: string;
dependencies: Record<string, DependencyItem>;
};
}
export class ArkTSConfigGenerator {
private static instance: ArkTSConfigGenerator | undefined;
private stdlibStdPath: string;
private stdlibEscompatPath: string;
private stdlibArkruntimePath: string;
private systemSdkPath: string;
private externalApiPath: string;
private interopApiPath: string;
private moduleInfos: Record<string, ModuleInfo>;
private pathSection: Record<string, string[]>;
private kitsDirectoryFiles: string[];
private constructor(buildConfig: BuildConfig, moduleInfos: Record<string, ModuleInfo>) {
let pandaSdkPath = path.resolve(buildConfig.buildSdkPath, PANDA_SDK_PATH_FROM_SDK);
let pandaStdlibPath: string = path.resolve(pandaSdkPath, 'lib', 'stdlib');
this.stdlibStdPath = path.resolve(pandaStdlibPath, 'std');
this.stdlibEscompatPath = path.resolve(pandaStdlibPath, 'escompat');
this.stdlibArkruntimePath = path.resolve(pandaStdlibPath, 'arkruntime');
this.systemSdkPath = path.resolve(buildConfig.buildSdkPath, SYSTEM_SDK_PATH_FROM_SDK);
this.externalApiPath = buildConfig.externalApiPath !== undefined ? buildConfig.externalApiPath : '';
this.interopApiPath = buildConfig.interopApiPath !== undefined ? buildConfig.interopApiPath : '';
this.moduleInfos = moduleInfos;
this.pathSection = {};
this.kitsDirectoryFiles = [];
}
public static getInstance(buildConfig?: BuildConfig, moduleInfos?: Record<string, ModuleInfo>): ArkTSConfigGenerator {
if (!ArkTSConfigGenerator.instance) {
if (!buildConfig || !moduleInfos) {
throw new Error('buildConfig and moduleInfos is required for the first instantiation of ArkTSConfigGenerator.');
}
ArkTSConfigGenerator.instance = new ArkTSConfigGenerator(buildConfig, moduleInfos);
}
return ArkTSConfigGenerator.instance;
}
public static getGenerator(buildConfig: BuildConfig, moduleInfos: Record<string, ModuleInfo>): ArkTSConfigGenerator {
return new ArkTSConfigGenerator(buildConfig, moduleInfos);
}
public static destroyInstance(): void {
ArkTSConfigGenerator.instance = undefined;
}
public getkitsDirectoryFiles(): string[] {
return this.kitsDirectoryFiles;
}
public getsystemSdkPath(): string {
return this.systemSdkPath;
}
private traverse(
pathSection: Record<string, string[] | DependencyItem>,
currentDir: string,
aliasConfigObj: Record<string, AliasConfig> | undefined,
prefix: string = '',
isInteropSdk: boolean = false,
relativePath: string = '',
isExcludedDir: boolean = false,
allowedExtensions: string[] = ['.d.ets'],
collectKitsFile: boolean = false
): void {
const items = fs.readdirSync(currentDir);
for (const item of items) {
const itemPath = path.join(currentDir, item);
const stat = fs.statSync(itemPath);
const isAllowedFile = allowedExtensions.some((ext) => item.endsWith(ext));
const separator = isInteropSdk ? '/' : '.';
if (stat.isFile() && !isAllowedFile) {
continue;
}
if (stat.isFile()) {
const basename = path.basename(item, '.d.ets');
const key = isExcludedDir ? basename : relativePath ? `${relativePath}${separator}${basename}` : basename;
pathSection[prefix + key] = isInteropSdk
? {
language: 'js',
path: itemPath,
ohmUrl: '',
alias: aliasConfigObj ? this.processAlias(basename, aliasConfigObj) : undefined
}
: [changeFileExtension(itemPath, '', '.d.ets')];
if (collectKitsFile && isAllowedFile) {
this.kitsDirectoryFiles.push(itemPath);
}
}
if (stat.isDirectory()) {
const isCurrentDirExcluded = path.basename(currentDir) === 'arkui' && item === 'runtime-api';
const newRelativePath = isCurrentDirExcluded ? '' : relativePath ? `${relativePath}${separator}${item}` : item;
this.traverse(
pathSection,
path.resolve(currentDir, item),
aliasConfigObj,
prefix,
isInteropSdk,
newRelativePath,
isCurrentDirExcluded || isExcludedDir
);
}
}
}
private generateSystemSdkPathSection(pathSection: Record<string, string[]>): void {
const directoryNames: string[] = ['api', 'arkts', 'kits'];
const collectPath = (sdkPath: string, dir: string): void => {
if (!fs.existsSync(sdkPath)) {
logger.debug(`sdk path ${sdkPath} not exist.`);
return;
}
if (dir === 'kits') {
this.traverse(pathSection, sdkPath, undefined, '', false, '', false, ['.d.ets'], true);
return;
}
this.traverse(pathSection, sdkPath, undefined);
};
directoryNames.forEach((dir) => {
const systemSdkPath = path.resolve(this.systemSdkPath, dir);
const externalApiPath = path.resolve(this.externalApiPath, dir);
collectPath(systemSdkPath, dir);
collectPath(externalApiPath, dir);
});
}
private getAlias(fullPath: string, entryRoot: string, packageName: string): string {
const normalizedFull = path.normalize(fullPath);
const normalizedEntry = path.normalize(entryRoot);
const entryDir = normalizedEntry.endsWith(path.sep) ? normalizedEntry : normalizedEntry + path.sep;
if (!normalizedFull.startsWith(entryDir)) {
throw new Error(`Path ${fullPath} is not under entry root ${entryRoot}`);
}
const relativePath = normalizedFull.substring(entryDir.length);
const formatPath = path.join(packageName, relativePath).replace(/\\/g, '/');
const alias = formatPath;
return changeFileExtension(alias, '');
}
private getPathSection(moduleInfo: ModuleInfo): Record<string, string[]> {
if (Object.keys(this.pathSection).length !== 0) {
return this.pathSection;
}
this.pathSection.std = [this.stdlibStdPath];
this.pathSection.escompat = [this.stdlibEscompatPath];
this.pathSection.arkruntime = [this.stdlibArkruntimePath];
this.generateSystemSdkPathSection(this.pathSection);
Object.values(moduleInfo.staticDepModuleInfos).forEach((depModuleName: string) => {
let depModuleInfo = this.moduleInfos[depModuleName];
if (depModuleInfo.language === LANGUAGE_VERSION.ARKTS_1_2) {
this.pathSection[depModuleInfo.packageName] = [path.resolve(depModuleInfo.moduleRootPath)];
} else if (depModuleInfo.language === LANGUAGE_VERSION.ARKTS_HYBRID) {
depModuleInfo.compileFiles.forEach((file) => {
const firstLine = fs.readFileSync(file, 'utf-8').split('\n')[0];
if (firstLine.includes('use static')) {
this.pathSection[this.getAlias(file, depModuleInfo.moduleRootPath, depModuleInfo.packageName)] = [
path.resolve(file)
];
}
});
}
});
if (moduleInfo.language === LANGUAGE_VERSION.ARKTS_HYBRID) {
moduleInfo.compileFiles.forEach((file) => {
const firstLine = fs.readFileSync(file, 'utf-8').split('\n')[0];
if (getFileLanguageVersion(firstLine) === LANGUAGE_VERSION.ARKTS_1_2) {
this.pathSection[this.getAlias(file, moduleInfo.moduleRootPath, moduleInfo.packageName)] = [
path.resolve(file)
];
}
});
}
return this.pathSection;
}
private getOhmurl(file: string, moduleInfo: ModuleInfo): string {
let unixFilePath: string = file.replace(/\\/g, '/');
let ohmurl: string = moduleInfo.packageName + '/' + unixFilePath;
return changeFileExtension(ohmurl, '');
}
private parseDeclFile(moduleInfo: ModuleInfo, dependencySection: Record<string, DependencyItem>): void {
if (!moduleInfo.declFilesPath || !fs.existsSync(moduleInfo.declFilesPath)) {
logger.debug(`Module ${moduleInfo.packageName} depends on dynamic module ${moduleInfo.packageName}, but
decl file not found on path ${moduleInfo.declFilesPath}`);
return;
}
let declFilesObject = JSON.parse(fs.readFileSync(moduleInfo.declFilesPath, 'utf-8'));
Object.keys(declFilesObject.files).forEach((file: string) => {
let ohmurl: string = this.getOhmurl(file, moduleInfo);
dependencySection[ohmurl] = {
language: 'js',
path: declFilesObject.files[file].declPath,
ohmUrl: declFilesObject.files[file].ohmUrl
};
let absFilePath: string = path.resolve(moduleInfo.moduleRootPath, file);
let entryFileWithoutExtension: string = changeFileExtension(moduleInfo.entryFile, '');
if (absFilePath === entryFileWithoutExtension) {
dependencySection[moduleInfo.packageName] = dependencySection[ohmurl];
}
});
}
private parseSdkAliasConfigFile(sdkAliasConfigFilePath?: string): Record<string, AliasConfig> | undefined {
if (!sdkAliasConfigFilePath) {
return;
}
const rawContent = fs.readFileSync(sdkAliasConfigFilePath, 'utf-8');
const jsonData = JSON5.parse(rawContent);
const aliasConfigObj: Record<string, AliasConfig> = {};
for (const [aliasKey, config] of Object.entries(jsonData)) {
const aliasConfig = config as AliasConfig;
aliasConfigObj[aliasKey] = aliasConfig;
}
return aliasConfigObj;
}
private processAlias(basename: string, aliasConfigObj: Record<string, AliasConfig>): string[] | undefined {
let alias: string[] = [];
for (const [aliasName, aliasConfig] of Object.entries(aliasConfigObj)) {
if (aliasConfig.isStatic) {
continue;
}
if (basename === aliasConfig.originalAPIName) {
alias.push(aliasName);
}
}
if (alias.length !== 0) {
return alias;
}
}
private generateSystemSdkDependenciesSection(
dependencySection: Record<string, DependencyItem>,
moduleInfo: ModuleInfo
): void {
const aliasConfigObj = this.parseSdkAliasConfigFile(moduleInfo.sdkAliasConfigPath);
let directoryNames: string[] = ['api', 'arkts', 'kits', 'component'];
directoryNames.forEach((dirName) => {
const basePath = path.resolve(this.interopApiPath, dirName);
if (!fs.existsSync(basePath)) {
logger.debug(`interop sdk path ${basePath} not exist.`);
return;
}
if (dirName === 'component') {
this.traverse(dependencySection, basePath, aliasConfigObj, 'component/', true);
} else {
this.traverse(dependencySection, basePath, aliasConfigObj, 'dynamic', true);
}
});
}
private getDependenciesSection(moduleInfo: ModuleInfo, dependencySection: Record<string, DependencyItem>): void {
this.generateSystemSdkDependenciesSection(dependencySection, moduleInfo);
let depModules: string[] = moduleInfo.dynamicDepModuleInfos;
depModules.forEach((depModuleName: string) => {
let depModuleInfo = this.moduleInfos[depModuleName];
this.parseDeclFile(depModuleInfo, dependencySection);
});
if (moduleInfo.language === LANGUAGE_VERSION.ARKTS_HYBRID) {
this.parseDeclFile(moduleInfo, dependencySection);
}
}
public writeArkTSConfigFile(moduleInfo: ModuleInfo): void {
let pathSection = this.getPathSection(moduleInfo);
let dependencySection: Record<string, DependencyItem> = {};
this.getDependenciesSection(moduleInfo, dependencySection);
let baseUrl: string = path.resolve(moduleInfo.moduleRootPath);
let arktsConfig: ArkTSConfigObject = {
compilerOptions: {
package: moduleInfo.packageName,
baseUrl: baseUrl,
paths: pathSection,
entry: moduleInfo.entryFile,
dependencies: dependencySection
}
};
ensurePathExists(moduleInfo.arktsConfigFile);
fs.writeFileSync(moduleInfo.arktsConfigFile, JSON.stringify(arktsConfig, null, 2), 'utf-8');
}
}