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 fs from 'fs'; 17import * as path from 'path'; 18import * as JSON5 from 'json5'; 19import { BuildConfig } from './types'; 20 21export interface ModuleDescriptor { 22 arktsversion: string; 23 name: string; 24 moduleType: string; 25 srcPath: string; 26} 27 28interface Json5Object { 29 module?: { 30 type?: string; 31 }; 32 modules?: Array<{ 33 name: string; 34 srcPath: string; 35 arktsversion?: string; 36 }>; 37 dependencies?: { 38 [packageName: string]: string; 39 }; 40} 41 42function removeTrailingCommas(jsonString: string): string { 43 return jsonString.replace(/,\s*([}\]])/g, '$1'); 44} 45 46function parseJson5(filePath: string): Json5Object { 47 try { 48 const rawContent = fs.readFileSync(filePath, 'utf8'); 49 const cleanedContent = removeTrailingCommas(rawContent); 50 return JSON5.parse(cleanedContent) as Json5Object; 51 } catch (error) { 52 console.error(`Error parsing ${filePath}:`, error); 53 return {} as Json5Object; 54 } 55} 56 57function getModuleTypeFromConfig(modulePath: string): string { 58 const moduleConfigPath = path.join(modulePath, 'src/main/module.json5'); 59 if (fs.existsSync(moduleConfigPath)) { 60 try { 61 const moduleData = parseJson5(moduleConfigPath); 62 return moduleData.module?.type || 'har'; 63 } catch (error) { 64 console.error(`Error parsing ${moduleConfigPath}:`, error); 65 } 66 } 67 return 'har'; 68} 69 70function getModulesFromBuildProfile(buildProfilePath: string): ModuleDescriptor[] { 71 if (!fs.existsSync(buildProfilePath)) { 72 console.error('Error: build-profile.json5 not found'); 73 process.exit(1); 74 } 75 76 const buildProfile = parseJson5(buildProfilePath); 77 const modules = buildProfile.modules || []; 78 79 return modules.map((module: { name: string; srcPath: string; arktsversion?: string }) => { 80 const moduleSrcPath = path.resolve(path.dirname(buildProfilePath), module.srcPath); 81 const arktsversion = module.arktsversion || '1.1'; 82 return { 83 name: module.name, 84 moduleType: getModuleTypeFromConfig(moduleSrcPath), 85 srcPath: moduleSrcPath, 86 arktsversion 87 }; 88 }); 89} 90 91function getEtsFiles(modulePath: string): string[] { 92 const files: string[] = []; 93 94 const shouldSkipDirectory = (relativePath: string): boolean => { 95 const testDir1 = `src${path.sep}test`; 96 const testDir2 = `src${path.sep}ohosTest`; 97 return relativePath.startsWith(testDir1) || relativePath.startsWith(testDir2); 98 }; 99 100 const processEntry = (dir: string, entry: fs.Dirent): void => { 101 const fullPath = path.join(dir, entry.name); 102 const relativePath = path.relative(modulePath, fullPath); 103 104 if (entry.isDirectory()) { 105 if (shouldSkipDirectory(relativePath)) { 106 return; 107 } 108 traverseDir(fullPath); 109 return; 110 } 111 112 if (entry.isFile() && entry.name.endsWith('.ets')) { 113 files.push(fullPath); 114 } 115 }; 116 117 const traverseDir = (dir: string): void => { 118 if (!fs.existsSync(dir)) { 119 return; 120 } 121 122 const entries = fs.readdirSync(dir, { withFileTypes: true }); 123 entries.forEach((entry) => processEntry(dir, entry)); 124 }; 125 126 traverseDir(modulePath); 127 return files; 128} 129 130function getModuleDependencies(modulePath: string, visited = new Set<string>()): string[] { 131 if (visited.has(modulePath)) { 132 return []; 133 } 134 visited.add(modulePath); 135 136 const extractDependencies = (): string[] => { 137 const packageFilePath = path.join(modulePath, 'oh-package.json5'); 138 if (!fs.existsSync(packageFilePath)) { 139 return []; 140 } 141 142 try { 143 const packageData = parseJson5(packageFilePath); 144 return Object.entries(packageData.dependencies || {}) 145 .map(([_, depPath]) => (depPath as string).replace('file:', '')) 146 .map((depPath) => path.resolve(modulePath, depPath)); 147 } catch (error) { 148 console.error(`Error parsing ${packageFilePath}:`, error); 149 return []; 150 } 151 }; 152 153 const resolveNestedDependencies = (deps: string[]): string[] => { 154 return deps.flatMap((depPath) => 155 visited.has(depPath) ? [] : [depPath, ...getModuleDependencies(depPath, visited)] 156 ); 157 }; 158 159 const dependencies = extractDependencies(); 160 const nestedDependencies = resolveNestedDependencies(dependencies); 161 return Array.from(new Set([...dependencies, ...nestedDependencies])); 162} 163 164function createMapEntryForPlugin(buildSdkPath: string, pluginName: string): string { 165 return path.join(buildSdkPath, 'build-tools', 'ui-plugins', 'lib', pluginName, 'index'); 166} 167 168function createPluginMap(buildSdkPath: string): Record<string, string> { 169 let pluginMap: Record<string, string> = {}; 170 const pluginList: string[] = ['ui-syntax-plugins', 'ui-plugins', 'memo-plugins']; 171 for (const plugin of pluginList) { 172 pluginMap[plugin] = createMapEntryForPlugin(buildSdkPath, plugin); 173 } 174 return pluginMap; 175} 176 177export function generateBuildConfigs( 178 buildSdkPath: string, 179 projectRoot: string, 180 modules?: ModuleDescriptor[] 181): Record<string, BuildConfig> { 182 const allBuildConfigs: Record<string, BuildConfig> = {}; 183 184 if (!modules) { 185 const buildProfilePath = path.join(projectRoot, 'build-profile.json5'); 186 modules = getModulesFromBuildProfile(buildProfilePath); 187 } 188 189 const definedModules = modules; 190 const cacheDir = path.join(projectRoot, '.idea', '.deveco'); 191 192 for (const module of definedModules) { 193 const modulePath = module.srcPath; 194 const compileFiles = new Set(getEtsFiles(modulePath)); 195 const pluginMap = createPluginMap(buildSdkPath); 196 197 // Get recursive dependencies 198 const dependencies = getModuleDependencies(modulePath); 199 for (const depPath of dependencies) { 200 getEtsFiles(depPath).forEach((file) => compileFiles.add(file)); 201 } 202 203 allBuildConfigs[module.name] = { 204 plugins: pluginMap, 205 arkts: {}, 206 arktsGlobal: {}, 207 compileFiles: Array.from(compileFiles), 208 packageName: module.name, 209 moduleType: module.moduleType, 210 buildType: 'build', 211 buildMode: 'Debug', 212 moduleRootPath: modulePath, 213 sourceRoots: ['./'], 214 hasMainModule: true, 215 loaderOutPath: path.join(modulePath, 'build', 'default', 'cache'), 216 cachePath: cacheDir, 217 buildSdkPath: buildSdkPath, 218 enableDeclgenEts2Ts: false, 219 declgenDtsOutPath: path.join(modulePath, 'build', 'default', 'cache'), 220 declgenTsOutPath: path.join(modulePath, 'build', 'default', 'cache'), 221 dependentModuleList: dependencies.map((dep) => { 222 const depModule = definedModules.find((m) => m.srcPath === dep); 223 return { 224 packageName: path.basename(dep), 225 moduleName: path.basename(dep), 226 moduleType: depModule ? depModule.moduleType : 'har', 227 modulePath: dep, 228 sourceRoots: ['./'], 229 entryFile: 'index.ets', 230 language: depModule ? depModule.arktsversion : '1.1' 231 }; 232 }) 233 }; 234 } 235 const outputPath = path.join(cacheDir, 'lsp_build_config.json'); 236 if (!fs.existsSync(cacheDir)) { 237 fs.mkdirSync(cacheDir, { recursive: true }); 238 } 239 fs.writeFileSync(outputPath, JSON.stringify(allBuildConfigs, null, 4)); 240 return allBuildConfigs; 241} 242