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