1/* 2 * Copyright (c) 2022-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 */ 15import * as path from 'node:path'; 16import * as fs from 'fs'; 17import * as ts from 'typescript'; 18import { EXTNAME_JS, EXTNAME_TS, EXTNAME_D_ETS, EXTNAME_D_TS, EXTNAME_ETS } from '../utils/consts/ExtensionName'; 19 20interface ResolutionContext { 21 sdkContext: SdkContext; 22 projectPath: string; 23 compilerOptions: ts.CompilerOptions; 24} 25 26interface SdkContext { 27 allSDKPath: string[]; 28 systemModules: string[]; 29 sdkConfigPrefix: string; 30 sdkDefaultApiPath: string; 31} 32 33export function readDeclareFiles(SdkPath: string): string[] { 34 if (SdkPath === '') { 35 return []; 36 } 37 const declarationsFileNames: string[] = []; 38 const declarationsPath = path.resolve(SdkPath, './build-tools/ets-loader/declarations'); 39 if (!fs.existsSync(declarationsPath)) { 40 throw new Error('get wrong sdkDefaultApiPath, declarationsPath not found'); 41 } 42 fs.readdirSync(declarationsPath).forEach((fileName: string) => { 43 if ((/\.d\.ts$/).test(fileName)) { 44 declarationsFileNames.push(path.resolve(SdkPath, './build-tools/ets-loader/declarations', fileName)); 45 } 46 }); 47 return declarationsFileNames; 48} 49 50export function createCompilerHost( 51 sdkDefaultApiPath: string, 52 sdkExternalApiPath: string[], 53 arktsWholeProjectPath: string 54): ts.CompilerHost { 55 const sdkContext: SdkContext = setSdkContext(sdkDefaultApiPath, sdkExternalApiPath); 56 const resolutionContext = createResolutionContext(sdkContext, arktsWholeProjectPath); 57 const customCompilerHost: ts.CompilerHost = { 58 getSourceFile(fileName: string, languageVersionOrOptions: ts.ScriptTarget): ts.SourceFile { 59 return ts.createSourceFile( 60 fileName, 61 this.readFile(fileName) || '', 62 languageVersionOrOptions, 63 true, 64 ts.ScriptKind.Unknown 65 ); 66 }, 67 getDefaultLibFileName: (option: ts.CompilerOptions) => { 68 return ts.getDefaultLibFilePath(option); 69 }, 70 writeFile: ts.sys.writeFile, 71 getCurrentDirectory: ts.sys.getCurrentDirectory, 72 getCanonicalFileName: (fileName: string) => { 73 return fileName; 74 }, 75 useCaseSensitiveFileNames: () => { 76 return ts.sys.useCaseSensitiveFileNames; 77 }, 78 fileExists: ts.sys.fileExists, 79 readFile: ts.sys.readFile, 80 readDirectory: ts.sys.readDirectory, 81 getNewLine: () => { 82 return ts.sys.newLine; 83 }, 84 resolveModuleNames: createModuleResolver(resolutionContext) 85 }; 86 return customCompilerHost; 87} 88 89function getResolveModule(modulePath: string, type: string): ts.ResolvedModuleFull { 90 return { 91 resolvedFileName: modulePath, 92 isExternalLibraryImport: false, 93 extension: ts.Extension[type as keyof typeof ts.Extension] 94 }; 95} 96 97const fileExistsCache: Map<string, boolean> = new Map<string, boolean>(); 98const dirExistsCache: Map<string, boolean> = new Map<string, boolean>(); 99const moduleResolutionHost: ts.ModuleResolutionHost = { 100 fileExists: (fileName: string): boolean => { 101 let exists = fileExistsCache.get(fileName); 102 if (exists === undefined) { 103 exists = ts.sys.fileExists(fileName); 104 fileExistsCache.set(fileName, exists); 105 } 106 return exists; 107 }, 108 directoryExists: (directoryName: string): boolean => { 109 let exists = dirExistsCache.get(directoryName); 110 if (exists === undefined) { 111 exists = ts.sys.directoryExists(directoryName); 112 dirExistsCache.set(directoryName, exists); 113 } 114 return exists; 115 }, 116 readFile(fileName: string): string | undefined { 117 return ts.sys.readFile(fileName); 118 }, 119 realpath(path: string): string { 120 if (ts.sys.realpath) { 121 return ts.sys.realpath(path); 122 } 123 return path; 124 }, 125 trace(s: string): void { 126 console.info(s); 127 } 128}; 129 130export interface ResolveModuleInfo { 131 modulePath: string; 132 isEts: boolean; 133} 134 135export function getRealModulePath(apiDirs: string, moduleName: string, exts: string[]): ResolveModuleInfo { 136 const resolveResult: ResolveModuleInfo = { 137 modulePath: '', 138 isEts: true 139 }; 140 const dir = apiDirs; 141 for (let i = 0; i < exts.length; i++) { 142 const ext = exts[i]; 143 const moduleDir = path.resolve(dir, moduleName + ext); 144 if (!fs.existsSync(moduleDir)) { 145 continue; 146 } 147 resolveResult.modulePath = moduleDir; 148 if (ext === EXTNAME_D_TS) { 149 resolveResult.isEts = false; 150 } 151 break; 152 } 153 154 return resolveResult; 155} 156 157export const shouldResolvedFiles: Set<string> = new Set(); 158export const resolvedModulesCache: Map<string, ts.ResolvedModuleFull[]> = new Map(); 159function createModuleResolver( 160 context: ResolutionContext 161): (moduleNames: string[], containingFile: string) => ts.ResolvedModuleFull[] { 162 return (moduleNames: string[], containingFile: string): ts.ResolvedModuleFull[] => { 163 return resolveModules(moduleNames, containingFile, context); 164 }; 165} 166 167function resolveModules( 168 moduleNames: string[], 169 containingFile: string, 170 context: ResolutionContext 171): ts.ResolvedModuleFull[] { 172 const resolvedModules: ts.ResolvedModuleFull[] = []; 173 const cacheKey = path.resolve(containingFile); 174 const cacheFileContent = resolvedModulesCache.get(cacheKey); 175 176 if (shouldResolveModules(moduleNames, containingFile, cacheFileContent)) { 177 for (const moduleName of moduleNames) { 178 const resolvedModule = resolveModule(moduleName, containingFile, context); 179 // @ts-expect-error null should push 180 resolvedModules.push(resolvedModule); 181 } 182 resolvedModulesCache.set(cacheKey, resolvedModules); 183 } else { 184 resolvedModulesCache.delete(cacheKey); 185 } 186 187 return resolvedModules; 188} 189 190function shouldResolveModules( 191 moduleNames: string[], 192 containingFile: string, 193 cacheFileContent: ts.ResolvedModuleFull[] | undefined 194): boolean { 195 const resolvedFilePath = path.resolve(containingFile); 196 const isCacheValid = cacheFileContent && cacheFileContent.length === moduleNames.length; 197 return ![...shouldResolvedFiles].length || shouldResolvedFiles.has(resolvedFilePath) || !isCacheValid; 198} 199 200function resolveModule( 201 moduleName: string, 202 containingFile: string, 203 context: ResolutionContext 204): ts.ResolvedModuleFull | null { 205 const result = ts.resolveModuleName(moduleName, containingFile, context.compilerOptions, moduleResolutionHost); 206 if (result.resolvedModule) { 207 return handleResolvedModule(result.resolvedModule); 208 } 209 210 if (isSdkModule(moduleName, context)) { 211 return resolveSdkModule(moduleName, context); 212 } 213 214 if (isEtsModule(moduleName)) { 215 return resolveEtsModule(moduleName, containingFile); 216 } 217 218 if (isTsModule(moduleName)) { 219 return resolveTsModule(moduleName, containingFile); 220 } 221 222 return resolveDefaultModule(moduleName, containingFile, context); 223} 224 225function handleResolvedModule(resolvedModule: ts.ResolvedModuleFull): ts.ResolvedModuleFull { 226 if (resolvedModule.resolvedFileName && path.extname(resolvedModule.resolvedFileName) === EXTNAME_JS) { 227 const detsPath = resolvedModule.resolvedFileName.replace(EXTNAME_JS, EXTNAME_D_ETS); 228 return ts.sys.fileExists(detsPath) ? getResolveModule(detsPath, EXTNAME_D_ETS) : resolvedModule; 229 } 230 return resolvedModule; 231} 232 233function isSdkModule(moduleName: string, context: ResolutionContext): boolean { 234 return new RegExp(`^@(${context.sdkContext.sdkConfigPrefix})\\.`, 'i').test(moduleName.trim()); 235} 236 237function resolveSdkModule(moduleName: string, context: ResolutionContext): ts.ResolvedModuleFull | null { 238 for (const sdkPath of context.sdkContext.allSDKPath) { 239 const resolveModuleInfo = getRealModulePath(sdkPath, moduleName, [EXTNAME_D_TS, EXTNAME_D_ETS]); 240 const modulePath = resolveModuleInfo.modulePath; 241 const isDETS = resolveModuleInfo.isEts; 242 243 if ( 244 context.sdkContext.systemModules.includes(moduleName + (isDETS ? EXTNAME_D_ETS : EXTNAME_D_TS)) && 245 ts.sys.fileExists(modulePath) 246 ) { 247 return getResolveModule(modulePath, isDETS ? EXTNAME_D_ETS : EXTNAME_D_TS); 248 } 249 } 250 return null; 251} 252 253function isEtsModule(moduleName: string): boolean { 254 return (/\.ets$/).test(moduleName) && !(/\.d\.ets$/).test(moduleName); 255} 256 257function resolveEtsModule(moduleName: string, containingFile: string): ts.ResolvedModuleFull | null { 258 const modulePath = path.resolve(path.dirname(containingFile), moduleName); 259 return ts.sys.fileExists(modulePath) ? getResolveModule(modulePath, EXTNAME_ETS) : null; 260} 261 262function isTsModule(moduleName: string): boolean { 263 return (/\.ts$/).test(moduleName); 264} 265 266function resolveTsModule(moduleName: string, containingFile: string): ts.ResolvedModuleFull | null { 267 const modulePath = path.resolve(path.dirname(containingFile), moduleName); 268 return ts.sys.fileExists(modulePath) ? getResolveModule(modulePath, EXTNAME_TS) : null; 269} 270 271function resolveDefaultModule( 272 moduleName: string, 273 containingFile: string, 274 context: ResolutionContext 275): ts.ResolvedModuleFull | null { 276 const { sdkContext } = context; 277 const { sdkDefaultApiPath } = sdkContext; 278 279 const paths = [ 280 path.resolve(sdkDefaultApiPath, './api', moduleName + EXTNAME_D_TS), 281 path.resolve(sdkDefaultApiPath, './api', moduleName + EXTNAME_D_ETS), 282 path.resolve(sdkDefaultApiPath, './kits', moduleName + EXTNAME_D_TS), 283 path.resolve(sdkDefaultApiPath, './kits', moduleName + EXTNAME_D_ETS), 284 path.resolve( 285 sdkDefaultApiPath, 286 './ets_loader/node_modules', 287 moduleName + ((/\./).test(moduleName) ? '' : EXTNAME_JS) 288 ), 289 path.resolve(sdkDefaultApiPath, './ets_loader/node_modules', moduleName + '/index.js'), 290 path.resolve(path.dirname(containingFile), (/\.d\.ets$/).test(moduleName) ? moduleName : moduleName + EXTNAME_D_ETS) 291 ]; 292 293 for (const filePath of paths) { 294 if (ts.sys.fileExists(filePath)) { 295 const ext = path.extname(filePath); 296 return getResolveModule(filePath, ext); 297 } 298 } 299 300 const srcIndex = containingFile.indexOf('src' + path.sep + 'main'); 301 if (srcIndex > 0) { 302 const detsModulePathFromModule = path.resolve( 303 containingFile.substring(0, srcIndex), 304 moduleName + path.sep + 'index' + EXTNAME_D_ETS 305 ); 306 return ts.sys.fileExists(detsModulePathFromModule) ? 307 getResolveModule(detsModulePathFromModule, EXTNAME_D_ETS) : 308 null; 309 } 310 311 return null; 312} 313 314function createResolutionContext(sdkContext: SdkContext, projectPath: string): ResolutionContext { 315 return { 316 sdkContext, 317 projectPath, 318 compilerOptions: createCompilerOptions(sdkContext, projectPath) 319 }; 320} 321 322function createCompilerOptions(sdkContext: SdkContext, projectPath: string): ts.CompilerOptions { 323 const compilerOptions: ts.CompilerOptions = ((): ts.CompilerOptions => { 324 const configPath = path.resolve(sdkContext.sdkDefaultApiPath, './build-tools/ets-loader/tsconfig.json'); 325 if (!fs.existsSync(configPath)) { 326 throw new Error('get wrong sdkDefaultApiPath, tsconfig.json not found'); 327 } 328 const configFile = ts.readConfigFile(configPath, ts.sys.readFile); 329 return configFile.config.compilerOptions; 330 })(); 331 Object.assign(compilerOptions, { 332 allowJs: true, 333 checkJs: false, 334 emitNodeModulesFiles: true, 335 importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Preserve, 336 module: ts.ModuleKind.CommonJS, 337 moduleResolution: ts.ModuleResolutionKind.NodeJs, 338 noEmit: true, 339 baseUrl: projectPath, 340 packageManagerType: 'ohpm' 341 }); 342 return compilerOptions; 343} 344function createInitialContext(sdkDefaultApiPath: string): SdkContext { 345 return { 346 allSDKPath: [], 347 systemModules: [], 348 sdkConfigPrefix: 'ohos|system|kit|arkts', 349 sdkDefaultApiPath 350 }; 351} 352 353function processBasePath(basePath: string): string[] { 354 if (!fs.existsSync(basePath)) { 355 return []; 356 } 357 return fs.readdirSync(basePath).filter((name) => { 358 return !name.startsWith('.'); 359 }); 360} 361 362function getBasePaths(sdkDefaultApiPath: string): string[] { 363 return [ 364 path.resolve(sdkDefaultApiPath, './api'), 365 path.resolve(sdkDefaultApiPath, './arkts'), 366 path.resolve(sdkDefaultApiPath, './kits') 367 ]; 368} 369 370function processExternalConfig(externalPath: string): { modules: string[]; paths: string[]; prefix?: string } | null { 371 const configPath = path.resolve(externalPath, 'sdkConfig.json'); 372 if (!fs.existsSync(configPath)) { 373 return null; 374 } 375 376 try { 377 const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); 378 if (!config.apiPath) { 379 return null; 380 } 381 382 const result = { 383 modules: [] as string[], 384 paths: [] as string[], 385 prefix: config.prefix?.replace(/^@/, '') 386 }; 387 388 config.apiPath.forEach((relPath: string) => { 389 const absPath = path.resolve(externalPath, relPath); 390 if (fs.existsSync(absPath)) { 391 result.modules.push(...processBasePath(absPath)); 392 result.paths.push(absPath); 393 } 394 }); 395 396 return result; 397 } catch (e) { 398 console.error(`Error processing SDK config: ${configPath}`, e); 399 return null; 400 } 401} 402 403function processExternalPaths(externalPaths: string[]): { modules: string[]; paths: string[]; prefixes: string[] } { 404 const result = { 405 modules: [] as string[], 406 paths: [] as string[], 407 prefixes: [] as string[] 408 }; 409 410 externalPaths.forEach((externalPath) => { 411 const configResult = processExternalConfig(externalPath); 412 if (!configResult) { 413 return; 414 } 415 416 result.modules.push(...configResult.modules); 417 result.paths.push(...configResult.paths); 418 if (configResult.prefix) { 419 result.prefixes.push(configResult.prefix); 420 } 421 }); 422 423 return result; 424} 425 426export function setSdkContext(sdkDefaultApiPath: string, sdkExternalApiPath: string[]): SdkContext { 427 const context = createInitialContext(sdkDefaultApiPath); 428 429 // Process base SDK paths 430 const basePaths = getBasePaths(sdkDefaultApiPath); 431 basePaths.forEach((p) => { 432 context.systemModules.push(...processBasePath(p)); 433 }); 434 context.allSDKPath.push(...basePaths); 435 436 // Process external SDK paths 437 const externalResults = processExternalPaths(sdkExternalApiPath); 438 context.systemModules.push(...externalResults.modules); 439 context.allSDKPath.push(...externalResults.paths); 440 if (externalResults.prefixes.length > 0) { 441 context.sdkConfigPrefix += `|${externalResults.prefixes.join('|')}`; 442 } 443 444 return context; 445} 446