• 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 { FileManager } from './interop_manager';
17import { ResolveModuleInfo, getResolveModule, readDeaclareFiles } from '../../../ets_checker';
18import {
19    mkdirsSync,
20    readFile,
21    toUnixPath
22} from '../../../utils';
23import {
24    ArkTSEvolutionModule,
25    BuildType,
26    DeclFilesConfig,
27    DECLGEN_CACHE_FILE,
28    Params,
29    ProjectConfig,
30    RunnerParms
31} from './type';
32import fs from 'fs';
33import path from 'path';
34import * as ts from 'typescript';
35import { EXTNAME_D_ETS, EXTNAME_JS } from '../common/ark_define';
36import { getRealModulePath } from '../../system_api/api_check_utils';
37import { generateInteropDecls } from 'declgen/build/src/generateInteropDecls';
38import { calculateFileHash } from '../utils';
39import { processInteropUI } from '../../../process_interop_ui';
40
41export function run(param: Params): boolean {
42    FileManager.init(param.dependentModuleMap);
43    DeclfileProductor.init(param);
44    param.tasks.forEach(task => {
45        const moduleInfo = FileManager.arkTSModuleMap.get(task.packageName);
46        if (moduleInfo?.dynamicFiles.length <= 0) {
47            return;
48        }
49        if (task.buildTask === BuildType.DECLGEN) {
50            DeclfileProductor.getInstance().runDeclgen(moduleInfo);
51        } else if (task.buildTask === BuildType.INTEROP_CONTEXT) {
52            DeclfileProductor.getInstance().writeDeclFileInfo(moduleInfo, task.mainModuleName);
53        } else if (task.buildTask === BuildType.BYTE_CODE_HAR) {
54            //todo
55        }
56    });
57    FileManager.cleanFileManagerObject();
58    return true;
59}
60
61class DeclfileProductor {
62    private static declFileProductor: DeclfileProductor;
63
64    static compilerOptions: ts.CompilerOptions;
65    static sdkConfigPrefix = 'ohos|system|kit|arkts';
66    static sdkConfigs = [];
67    static systemModules = [];
68    static defaultSdkConfigs = [];
69    static projectPath;
70    private projectConfig;
71    private pkgDeclFilesConfig: { [pkgName: string]: DeclFilesConfig } = {};
72
73    static init(param: Params): void {
74        DeclfileProductor.declFileProductor = new DeclfileProductor(param);
75        DeclfileProductor.compilerOptions = ts.readConfigFile(
76            path.join(__dirname, '../../../../tsconfig.json'), ts.sys.readFile).config.compilerOptions;
77        DeclfileProductor.initSdkConfig();
78        Object.assign(DeclfileProductor.compilerOptions, {
79            emitNodeModulesFiles: true,
80            importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Preserve,
81            module: ts.ModuleKind.CommonJS,
82            moduleResolution: ts.ModuleResolutionKind.NodeJs,
83            noEmit: true,
84            packageManagerType: 'ohpm',
85            allowJs: true,
86            allowSyntheticDefaultImports: true,
87            esModuleInterop: true,
88            noImplicitAny: false,
89            noUnusedLocals: false,
90            noUnusedParameters: false,
91            experimentalDecorators: true,
92            resolveJsonModule: true,
93            skipLibCheck: false,
94            sourceMap: true,
95            target: 8,
96            types: [],
97            typeRoots: [],
98            lib: ['lib.es2021.d.ts'],
99            alwaysStrict: true,
100            checkJs: false,
101            maxFlowDepth: 2000,
102            etsAnnotationsEnable: false,
103            etsLoaderPath: path.join(__dirname, '../../../'),
104            needDoArkTsLinter: true,
105            isCompatibleVersion: false,
106            skipTscOhModuleCheck: false,
107            skipArkTSStaticBlocksCheck: false,
108            incremental: true,
109            tsImportSendableEnable: false,
110            skipPathsInKeyForCompilationSettings: true,
111        });
112        DeclfileProductor.projectPath = param.projectConfig.projectRootPath;
113    }
114    static getInstance(param?: Params): DeclfileProductor {
115        if (!this.declFileProductor) {
116            this.declFileProductor = new DeclfileProductor(param);
117        }
118        return this.declFileProductor;
119    }
120
121    private constructor(param: Params) {
122        this.projectConfig = param.projectConfig as ProjectConfig;
123    }
124
125    runDeclgen(moduleInfo: ArkTSEvolutionModule): void {
126        const cachePath = `${moduleInfo.declgenV2OutPath}/.${DECLGEN_CACHE_FILE}`;
127        let existingCache = {};
128        const filesToProcess = [];
129
130        if (fs.existsSync(cachePath)) {
131            existingCache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
132        }
133
134        let inputList = [];
135        let hashMap = {};
136        moduleInfo.dynamicFiles.forEach(path => {
137            let unixPath = toUnixPath(path);
138            const fileHash = calculateFileHash(path);
139            if (!existingCache[unixPath] || existingCache[unixPath] !== fileHash) {
140                filesToProcess.push(unixPath);
141                hashMap[unixPath] = fileHash;
142            }
143        });
144        if (filesToProcess.length === 0) {
145            return;
146        }
147        readDeaclareFiles().forEach(path => {
148            filesToProcess.push(toUnixPath(path));
149        });
150
151        inputList = inputList.filter(filePath => !filePath.endsWith('.js'));
152        const config: RunnerParms = {
153            inputDirs: [],
154            inputFiles: filesToProcess,
155            outDir: moduleInfo.declgenV2OutPath,
156            // use package name as folder name
157            rootDir: moduleInfo.modulePath,
158            customResolveModuleNames: resolveModuleNames,
159            customCompilerOptions: DeclfileProductor.compilerOptions,
160            includePaths: [moduleInfo.modulePath]
161        };
162        if (!fs.existsSync(config.outDir)) {
163            fs.mkdirSync(config.outDir, { recursive: true });
164        }
165        fs.mkdirSync(config.outDir, { recursive: true });
166        generateInteropDecls(config);
167        processInteropUI(FileManager.arkTSModuleMap.get(moduleInfo.packageName)?.declgenV2OutPath);
168        const newCache = {
169            ...existingCache,
170            ...hashMap
171        };
172        fs.writeFileSync(cachePath, JSON.stringify(newCache, null, 2));
173    }
174
175    writeDeclFileInfo(moduleInfo: ArkTSEvolutionModule, mainModuleName: string): void {
176      moduleInfo.isNative = moduleInfo.isNative ?? moduleInfo.packageName.endsWith('.so');
177      moduleInfo.dynamicFiles.forEach(file => {
178        this.addDeclFilesConfig(file, mainModuleName, this.projectConfig.bundleName, moduleInfo);
179      });
180
181      const declFilesConfigFile: string = toUnixPath(moduleInfo.declFilesPath);
182      mkdirsSync(path.dirname(declFilesConfigFile));
183      if (this.pkgDeclFilesConfig[moduleInfo.packageName]) {
184        fs.writeFileSync(declFilesConfigFile, JSON.stringify(this.pkgDeclFilesConfig[moduleInfo.packageName], null, 2), 'utf-8');
185      }
186    }
187
188    addDeclFilesConfig(filePath: string, mainModuleName: string, bundleName: string, moduleInfo: ArkTSEvolutionModule): void {
189        const projectFilePath = getRelativePath(filePath, moduleInfo.modulePath);
190
191        const declgenV2OutPath: string = this.getDeclgenV2OutPath(moduleInfo.packageName);
192        if (!declgenV2OutPath) {
193            return;
194        }
195        if (!this.pkgDeclFilesConfig[moduleInfo.packageName]) {
196            this.pkgDeclFilesConfig[moduleInfo.packageName] = { packageName: moduleInfo.packageName, files: {} };
197        }
198        if (filePath.endsWith(EXTNAME_JS)) {
199            return;
200        }
201        if (this.pkgDeclFilesConfig[moduleInfo.packageName].files[projectFilePath]) {
202            return;
203        }
204        // The module name of the entry module of the project during the current compilation process.
205        const normalizedFilePath: string = moduleInfo.isNative
206          ? moduleInfo.moduleName
207          : `${moduleInfo.packageName}/${projectFilePath}`;
208        const declPath: string = path.join(toUnixPath(declgenV2OutPath), projectFilePath) + EXTNAME_D_ETS;
209        const isNativeFlag = moduleInfo.isNative ? 'Y' : 'N';
210        const ohmUrl: string = `${isNativeFlag}&${mainModuleName}&${bundleName}&${normalizedFilePath}&${moduleInfo.packageVersion}`;
211        this.pkgDeclFilesConfig[moduleInfo.packageName].files[projectFilePath] = { declPath, ohmUrl: `@normalized:${ohmUrl}` };
212    }
213
214    getDeclgenV2OutPath(pkgName: string): string {
215        if (FileManager.arkTSModuleMap.size && FileManager.arkTSModuleMap.get(pkgName)) {
216            const arkTsModuleInfo: ArkTSEvolutionModule = FileManager.arkTSModuleMap.get(pkgName);
217            return arkTsModuleInfo.declgenV2OutPath;
218        }
219        return '';
220    }
221
222    static initSdkConfig(): void {
223        const apiDirPath = path.resolve(__dirname, '../../../../../api');
224        const arktsDirPath = path.resolve(__dirname, '../../../../../arkts');
225        const kitsDirPath = path.resolve(__dirname, '../../../../../kits');
226        const systemModulePathArray = [apiDirPath];
227        if (!process.env.isFaMode) {
228            systemModulePathArray.push(arktsDirPath, kitsDirPath);
229        }
230        systemModulePathArray.forEach(systemModulesPath => {
231            if (fs.existsSync(systemModulesPath)) {
232                const modulePaths = [];
233                readFile(systemModulesPath, modulePaths);
234                DeclfileProductor.systemModules.push(...fs.readdirSync(systemModulesPath));
235                const moduleSubdir = modulePaths.filter(filePath => {
236                    const dirName = path.dirname(filePath);
237                    return !(dirName === apiDirPath || dirName === arktsDirPath || dirName === kitsDirPath);
238                }).map(filePath => {
239                    return filePath
240                        .replace(apiDirPath, '')
241                        .replace(arktsDirPath, '')
242                        .replace(kitsDirPath, '')
243                        .replace(/(^\\)|(.d.e?ts$)/g, '')
244                        .replace(/\\/g, '/');
245                });
246            }
247        });
248        DeclfileProductor.defaultSdkConfigs = [
249            {
250                'apiPath': systemModulePathArray,
251                'prefix': '@ohos'
252            }, {
253                'apiPath': systemModulePathArray,
254                'prefix': '@system'
255            }, {
256                'apiPath': systemModulePathArray,
257                'prefix': '@arkts'
258            }
259        ];
260        DeclfileProductor.sdkConfigs = [...DeclfileProductor.defaultSdkConfigs];
261    }
262}
263
264function resolveModuleNames(moduleNames: string[], containingFile: string): ts.ResolvedModuleFull[] {
265    const resolvedModules: ts.ResolvedModuleFull[] = [];
266
267    for (const moduleName of moduleNames) {
268        let resolvedModule: ts.ResolvedModuleFull | null = null;
269
270        resolvedModule = resolveWithDefault(moduleName, containingFile);
271        if (resolvedModule) {
272            resolvedModules.push(resolvedModule);
273            continue;
274        }
275
276        resolvedModule = resolveSdkModule(moduleName);
277        if (resolvedModule) {
278            resolvedModules.push(resolvedModule);
279            continue;
280        }
281
282        resolvedModule = resolveEtsModule(moduleName, containingFile);
283        if (resolvedModule) {
284            resolvedModules.push(resolvedModule);
285            continue;
286        }
287
288        resolvedModule = resolveTsModule(moduleName, containingFile);
289        if (resolvedModule) {
290            resolvedModules.push(resolvedModule);
291            continue;
292        }
293
294        resolvedModule = resolveOtherModule(moduleName, containingFile);
295        resolvedModules.push(resolvedModule ?? null);
296    }
297
298    return resolvedModules;
299}
300
301function resolveWithDefault(
302    moduleName: string,
303    containingFile: string
304): ts.ResolvedModuleFull | null {
305    const result = ts.resolveModuleName(moduleName, containingFile, DeclfileProductor.compilerOptions, moduleResolutionHost);
306    if (!result.resolvedModule) {
307        return null;
308    }
309
310    const resolvedFileName = result.resolvedModule.resolvedFileName;
311    if (resolvedFileName && path.extname(resolvedFileName) === EXTNAME_JS) {
312        const resultDETSPath = resolvedFileName.replace(EXTNAME_JS, EXTNAME_D_ETS);
313        if (ts.sys.fileExists(resultDETSPath)) {
314            return getResolveModule(resultDETSPath, EXTNAME_D_ETS);
315        }
316    }
317
318    return result.resolvedModule;
319}
320
321function resolveEtsModule(moduleName: string, containingFile: string): ts.ResolvedModuleFull | null {
322    if (!/\.ets$/.test(moduleName) || /\.d\.ets$/.test(moduleName)) {
323        return null;
324    }
325
326    const modulePath = path.resolve(path.dirname(containingFile), moduleName);
327    return ts.sys.fileExists(modulePath) ? getResolveModule(modulePath, '.ets') : null;
328}
329
330function resolveSdkModule(moduleName: string): ts.ResolvedModuleFull | null {
331    const prefixRegex = new RegExp(`^@(${DeclfileProductor.sdkConfigPrefix})\\.`, 'i');
332    if (!prefixRegex.test(moduleName.trim())) {
333        return null;
334    }
335
336    for (const sdkConfig of DeclfileProductor.sdkConfigs) {
337        const resolveModuleInfo: ResolveModuleInfo = getRealModulePath(sdkConfig.apiPath, moduleName, ['.d.ts', '.d.ets']);
338        const modulePath: string = resolveModuleInfo.modulePath;
339        const isDETS: boolean = resolveModuleInfo.isEts;
340
341        const moduleKey = moduleName + (isDETS ? '.d.ets' : '.d.ts');
342        if (DeclfileProductor.systemModules.includes(moduleKey) && ts.sys.fileExists(modulePath)) {
343            return getResolveModule(modulePath, isDETS ? '.d.ets' : '.d.ts');
344        }
345    }
346
347    return null;
348}
349
350function resolveTsModule(moduleName: string, containingFile: string): ts.ResolvedModuleFull | null {
351    if (!/\.ts$/.test(moduleName)) {
352        return null;
353    }
354
355
356    const modulePath = path.resolve(path.dirname(containingFile), moduleName);
357    return ts.sys.fileExists(modulePath) ? getResolveModule(modulePath, '.ts') : null;
358}
359
360function resolveOtherModule(moduleName: string, containingFile: string): ts.ResolvedModuleFull | null {
361    const apiModulePath = path.resolve(__dirname, '../../../api', moduleName + '.d.ts');
362    const systemDETSModulePath = path.resolve(__dirname, '../../../api', moduleName + '.d.ets');
363    const kitModulePath = path.resolve(__dirname, '../../../kits', moduleName + '.d.ts');
364    const kitSystemDETSModulePath = path.resolve(__dirname, '../../../kits', moduleName + '.d.ets');
365    const jsModulePath = path.resolve(__dirname, '../node_modules', moduleName + (moduleName.endsWith('.js') ? '' : '.js'));
366    const fileModulePath = path.resolve(__dirname, '../node_modules', moduleName + '/index.js');
367    const DETSModulePath = path.resolve(path.dirname(containingFile),
368        moduleName.endsWith('.d.ets') ? moduleName : moduleName + EXTNAME_D_ETS);
369
370    if (ts.sys.fileExists(apiModulePath)) {
371        return getResolveModule(apiModulePath, '.d.ts');
372    } else if (ts.sys.fileExists(systemDETSModulePath)) {
373        return getResolveModule(systemDETSModulePath, '.d.ets');
374    } else if (ts.sys.fileExists(kitModulePath)) {
375        return getResolveModule(kitModulePath, '.d.ts');
376    } else if (ts.sys.fileExists(kitSystemDETSModulePath)) {
377        return getResolveModule(kitSystemDETSModulePath, '.d.ets');
378    } else if (ts.sys.fileExists(jsModulePath)) {
379        return getResolveModule(jsModulePath, '.js');
380    } else if (ts.sys.fileExists(fileModulePath)) {
381        return getResolveModule(fileModulePath, '.js');
382    } else if (ts.sys.fileExists(DETSModulePath)) {
383        return getResolveModule(DETSModulePath, '.d.ets');
384    } else {
385        const srcIndex = DeclfileProductor.projectPath.indexOf('src' + path.sep + 'main');
386        if (srcIndex > 0) {
387            const DETSModulePathFromModule = path.resolve(
388                DeclfileProductor.projectPath.substring(0, srcIndex),
389                moduleName + path.sep + 'index' + EXTNAME_D_ETS
390            );
391            if (ts.sys.fileExists(DETSModulePathFromModule)) {
392                return getResolveModule(DETSModulePathFromModule, '.d.ets');
393            }
394        }
395        return null;
396    }
397}
398
399function getRelativePath(filePath: string, pkgPath: string): string {
400    // rollup uses commonjs plugin to handle commonjs files,
401    // the commonjs files are prefixed with '\x00' and need to be removed.
402    if (filePath.startsWith('\x00')) {
403        filePath = filePath.replace('\x00', '');
404    }
405    let unixFilePath: string = toUnixPath(filePath);
406
407    // Handle .d.ets and .d.ts extensions
408    const dEtsIndex = unixFilePath.lastIndexOf('.d.ets');
409    const dTsIndex = unixFilePath.lastIndexOf('.d.ts');
410
411    if (dEtsIndex !== -1) {
412        unixFilePath = unixFilePath.substring(0, dEtsIndex);
413    } else if (dTsIndex !== -1) {
414        unixFilePath = unixFilePath.substring(0, dTsIndex);
415    } else {
416        // Fallback to regular extension removal if not a .d file
417        const lastDotIndex = unixFilePath.lastIndexOf('.');
418        if (lastDotIndex !== -1) {
419            unixFilePath = unixFilePath.substring(0, lastDotIndex);
420        }
421    }
422
423    const projectFilePath: string = unixFilePath.replace(toUnixPath(pkgPath) + '/', '');
424    return projectFilePath;
425}
426
427const moduleResolutionHost: ts.ModuleResolutionHost = {
428    fileExists: (fileName: string): boolean => {
429        let exists = ts.sys.fileExists(fileName);
430        if (exists === undefined) {
431            exists = ts.sys.fileExists(fileName);
432        }
433        return exists;
434    },
435
436    readFile(fileName: string): string | undefined {
437        return ts.sys.readFile(fileName);
438    },
439    realpath(path: string): string {
440        return ts.sys.realpath(path);
441    },
442    trace(s: string): void {
443        console.info(s);
444    }
445};