From 7dbfcc73f75d9c8552e1bb39ffeb7db18bd21b4d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 20:45:20 +0000 Subject: [PATCH 1/2] fix: resolve UIDesignKit exports such as .titleBar after hmsPath is set HMS kit barrels resolved once hmsPath was configured, but nested @hms.* re-exports and ets/component attribute types (HdsNavigation.titleBar) were never indexed. Recursively map HMS api/kits/arkts declarations and include HMS component libs. Co-authored-by: Tony Coder <407243179@qq.com> --- .changeset/uidesignkit-titlebar.md | 5 ++ .../src/classes/config-resolver.ts | 64 ++++++++++++------- .../language-server/src/utils/hms-paths.ts | 52 +++++++++++++++ .../language-server/test/hms-paths.test.ts | 53 +++++++++++++++ 4 files changed, 151 insertions(+), 23 deletions(-) create mode 100644 .changeset/uidesignkit-titlebar.md create mode 100644 packages/language-server/src/utils/hms-paths.ts create mode 100644 packages/language-server/test/hms-paths.test.ts diff --git a/.changeset/uidesignkit-titlebar.md b/.changeset/uidesignkit-titlebar.md new file mode 100644 index 00000000..4c015599 --- /dev/null +++ b/.changeset/uidesignkit-titlebar.md @@ -0,0 +1,5 @@ +--- +"@arkts/language-server": patch +--- + +fix: resolve UIDesignKit exports such as `.titleBar` after `hmsPath` is set diff --git a/packages/language-server/src/classes/config-resolver.ts b/packages/language-server/src/classes/config-resolver.ts index a9876c6c..8348ed9b 100644 --- a/packages/language-server/src/classes/config-resolver.ts +++ b/packages/language-server/src/classes/config-resolver.ts @@ -12,6 +12,7 @@ import { createRelativePattern, Uri } from '@vstils/core' import { FileType } from '@vstils/fs' import defu from 'defu' import * as ets from 'ohos-typescript' +import { addHmsPathMapping, hmsDeclarationToModuleNames, hmsEtsWildcardTargets, stripDeclarationExtension } from '../utils/hms-paths' export class ConfigResolver { constructor( @@ -153,39 +154,51 @@ export class ConfigResolver { const declarationsUri = Uri.joinPath(Uri.file(this.getEtsLoaderPath()), 'declarations') const globalFiles = await this.fs.glob(createRelativePattern(declarationsUri, '**/*.d.ts')).then(uris => uris.map(uri => uri.fsPath)) - return [...dtsFiles, ...detsFiles, ...globalFiles, ...await this.getTsdkLib()].filter((item, index, self) => self.indexOf(item) === index && Boolean(item)) + return [...dtsFiles, ...detsFiles, ...globalFiles, ...await this.getHmsComponentLib(), ...await this.getTsdkLib()].filter((item, index, self) => self.indexOf(item) === index && Boolean(item)) } - private getFileNameWithoutExtension(fileNameWithExtension: string): string { - if (fileNameWithExtension.endsWith('.d.ts') || fileNameWithExtension.endsWith('.d.ets')) { - return fileNameWithExtension.replace(/\.d\.ts$/, '').replace(/\.d\.ets$/, '') + /** + * HMS UI components (e.g. `HdsNavigationAttribute.titleBar`) live in + * `ets/component`, same as OpenHarmony built-ins. Without these files in + * `lib`, `@kit.UIDesignKit` resolves but chained attributes stay unresolved. + */ + private async getHmsComponentLib(): Promise { + const hmsSdkPath = this.getHmsSdkPath() + if (!hmsSdkPath) return [] + const hmsComponentFolderUri = Uri.joinPath(Uri.file(hmsSdkPath), 'ets', 'component') + if (!await this.isDirectory(hmsComponentFolderUri)) return [] + const dtsFiles = await this.fs.glob(createRelativePattern(hmsComponentFolderUri, '**/*.d.ts')).then(uris => uris.map(uri => uri.fsPath)) + const detsFiles = await this.fs.glob(createRelativePattern(hmsComponentFolderUri, '**/*.d.ets')).then(uris => uris.map(uri => uri.fsPath)) + return [...dtsFiles, ...detsFiles] + } + + private async globHmsDeclarationFiles(folder: Uri): Promise { + if (!await this.isDirectory(folder)) return [] + const dtsFiles = await this.fs.glob(createRelativePattern(folder, '**/*.d.ts')).then(uris => uris.map(uri => uri.fsPath)) + const detsFiles = await this.fs.glob(createRelativePattern(folder, '**/*.d.ets')).then(uris => uris.map(uri => uri.fsPath)) + return [...dtsFiles, ...detsFiles] + } + + private async mapHmsDeclarationFolder(folder: Uri, paths: import('typescript').MapLike): Promise { + const files = await this.globHmsDeclarationFiles(folder) + const folderPath = folder.fsPath + for (const filePath of files) { + const relativePath = path.relative(folderPath, filePath) + const wildcardTarget = Uri.joinPath(Uri.file(stripDeclarationExtension(filePath)), '*').fsPath + for (const moduleName of hmsDeclarationToModuleNames(relativePath)) + addHmsPathMapping(paths, moduleName, filePath, wildcardTarget) } - return path.basename(fileNameWithExtension, path.extname(fileNameWithExtension)) } private async hmsToTypeScriptCompilerOptionsPaths(): Promise> { try { const hmsSdkPath = this.getHmsSdkPath() if (!hmsSdkPath) return {} - const hmsApiFolder = Uri.joinPath(Uri.file(hmsSdkPath), 'ets', 'api') - const hmsKitsFolder = Uri.joinPath(Uri.file(hmsSdkPath), 'ets', 'kits') - if (!hmsApiFolder || !hmsKitsFolder) return {} - + const hmsEtsFolder = Uri.joinPath(Uri.file(hmsSdkPath), 'ets') const paths: import('typescript').MapLike = {} - const apiFiles = await this.fs.readDirectory(hmsApiFolder) - const kitsFiles = await this.fs.readDirectory(hmsKitsFolder) - for (const [fileNameWithExtension, fileType] of apiFiles) { - if (fileType !== FileType.File) continue - const fileName = this.getFileNameWithoutExtension(fileNameWithExtension) - paths[fileName] = [Uri.joinPath(hmsApiFolder, fileNameWithExtension).fsPath] - paths[`${fileName}/*`] = [Uri.joinPath(hmsApiFolder, fileNameWithExtension, '*').fsPath] - } - for (const [fileNameWithExtension, fileType] of kitsFiles) { - if (fileType !== FileType.File) continue - const fileName = this.getFileNameWithoutExtension(fileNameWithExtension) - paths[fileName] = [Uri.joinPath(hmsKitsFolder, fileNameWithExtension).fsPath] - paths[`${fileName}/*`] = [Uri.joinPath(hmsKitsFolder, fileNameWithExtension, '*').fsPath] - } + await this.mapHmsDeclarationFolder(Uri.joinPath(hmsEtsFolder, 'api'), paths) + await this.mapHmsDeclarationFolder(Uri.joinPath(hmsEtsFolder, 'kits'), paths) + await this.mapHmsDeclarationFolder(Uri.joinPath(hmsEtsFolder, 'arkts'), paths) return paths } catch (error) { @@ -197,11 +210,16 @@ export class ConfigResolver { } async getPaths(): Promise> { + const hmsSdkPath = this.getHmsSdkPath() + const hmsWildcards = hmsSdkPath + ? hmsEtsWildcardTargets(Uri.joinPath(Uri.file(hmsSdkPath), 'ets').fsPath) + : [] return { '*': [ './api/*', './kits/*', './arkts/*', + ...hmsWildcards, ].filter(Boolean) as string[], '@internal/full/*': ['./api/@internal/full/*'], ...await this.hmsToTypeScriptCompilerOptionsPaths(), diff --git a/packages/language-server/src/utils/hms-paths.ts b/packages/language-server/src/utils/hms-paths.ts new file mode 100644 index 00000000..a6211275 --- /dev/null +++ b/packages/language-server/src/utils/hms-paths.ts @@ -0,0 +1,52 @@ +/** + * HMS kit barrels (e.g. `@kit.UIDesignKit`) re-export symbols such as + * `HdsNavigation` / `HdsNavigationAttribute` (which declare `.titleBar`) + * from `@hms.*` modules. Those modules may live in nested folders while + * imports still use dotted specifiers. + */ + +export function stripDeclarationExtension(fileName: string): string { + return fileName + .replace(/\\/g, '/') + .replace(/\.d\.ets$/i, '') + .replace(/\.d\.ts$/i, '') +} + +/** + * Module specifiers for one HMS declaration file, relative to `ets/api`, + * `ets/kits`, or `ets/arkts`. + * + * `api/@hms.core.uidesign/titleBar.d.ts` yields both + * `@hms.core.uidesign/titleBar` and `@hms.core.uidesign.titleBar`. + */ +export function hmsDeclarationToModuleNames(relativePath: string): string[] { + const moduleName = stripDeclarationExtension(relativePath) + const posixRelative = relativePath.replace(/\\/g, '/') + if (!moduleName || moduleName === posixRelative) + return [] + + const names = new Set([moduleName]) + if (moduleName.includes('/')) + names.add(moduleName.replaceAll('/', '.')) + return [...names] +} + +export function addHmsPathMapping( + paths: Record, + moduleName: string, + filePath: string, + wildcardTarget: string, +): void { + const existing = paths[moduleName] + if (!existing) + paths[moduleName] = [filePath] + else if (!existing.includes(filePath)) + existing.push(filePath) + + paths[`${moduleName}/*`] = [wildcardTarget] +} + +export function hmsEtsWildcardTargets(hmsEtsPath: string): string[] { + const normalized = hmsEtsPath.replace(/\\/g, '/') + return ['api', 'kits', 'arkts'].map(dir => `${normalized}/${dir}/*`) +} diff --git a/packages/language-server/test/hms-paths.test.ts b/packages/language-server/test/hms-paths.test.ts new file mode 100644 index 00000000..75be83db --- /dev/null +++ b/packages/language-server/test/hms-paths.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vite-plus/test' +import { addHmsPathMapping, hmsDeclarationToModuleNames, hmsEtsWildcardTargets, stripDeclarationExtension } from '../src/utils/hms-paths' + +describe('hms kit export path mapping', () => { + it('strips .d.ts and .d.ets extensions', () => { + expect(stripDeclarationExtension('@kit.UIDesignKit.d.ts')).toBe('@kit.UIDesignKit') + expect(stripDeclarationExtension('@hms.core.uidesign.titleBar.d.ets')).toBe('@hms.core.uidesign.titleBar') + expect(stripDeclarationExtension('nested\\titleBar.d.ts')).toBe('nested/titleBar') + }) + + it('maps a top-level kit barrel to its module name', () => { + expect(hmsDeclarationToModuleNames('@kit.UIDesignKit.d.ts')).toEqual(['@kit.UIDesignKit']) + }) + + it('maps nested HMS api files to both slash and dotted specifiers', () => { + expect(hmsDeclarationToModuleNames('@hms.core.uidesign/titleBar.d.ts')).toEqual([ + '@hms.core.uidesign/titleBar', + '@hms.core.uidesign.titleBar', + ]) + expect(hmsDeclarationToModuleNames('@hms.core.uidesign/HdsNavigation.d.ets')).toEqual([ + '@hms.core.uidesign/HdsNavigation', + '@hms.core.uidesign.HdsNavigation', + ]) + }) + + it('ignores non-declaration files', () => { + expect(hmsDeclarationToModuleNames('readme.md')).toEqual([]) + }) + + it('records file and wildcard targets for kit re-exports', () => { + const paths: Record = {} + addHmsPathMapping( + paths, + '@hms.core.uidesign.titleBar', + '/hms/ets/api/@hms.core.uidesign/titleBar.d.ts', + '/hms/ets/api/@hms.core.uidesign/titleBar/*', + ) + expect(paths['@hms.core.uidesign.titleBar']).toEqual([ + '/hms/ets/api/@hms.core.uidesign/titleBar.d.ts', + ]) + expect(paths['@hms.core.uidesign.titleBar/*']).toEqual([ + '/hms/ets/api/@hms.core.uidesign/titleBar/*', + ]) + }) + + it('adds HMS api/kits/arkts wildcards so kit re-exports resolve like OpenHarmony', () => { + expect(hmsEtsWildcardTargets('/sdk/hms/ets')).toEqual([ + '/sdk/hms/ets/api/*', + '/sdk/hms/ets/kits/*', + '/sdk/hms/ets/arkts/*', + ]) + }) +}) From d7312e889f2cd530b845bafc94e25a3cc15d912c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 20:47:24 +0000 Subject: [PATCH 2/2] style: satisfy naily if-oneline in HMS path helpers Co-authored-by: Tony Coder <407243179@qq.com> --- packages/language-server/src/utils/hms-paths.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/language-server/src/utils/hms-paths.ts b/packages/language-server/src/utils/hms-paths.ts index a6211275..1c348585 100644 --- a/packages/language-server/src/utils/hms-paths.ts +++ b/packages/language-server/src/utils/hms-paths.ts @@ -22,12 +22,10 @@ export function stripDeclarationExtension(fileName: string): string { export function hmsDeclarationToModuleNames(relativePath: string): string[] { const moduleName = stripDeclarationExtension(relativePath) const posixRelative = relativePath.replace(/\\/g, '/') - if (!moduleName || moduleName === posixRelative) - return [] + if (!moduleName || moduleName === posixRelative) return [] const names = new Set([moduleName]) - if (moduleName.includes('/')) - names.add(moduleName.replaceAll('/', '.')) + if (moduleName.includes('/')) names.add(moduleName.replaceAll('/', '.')) return [...names] } @@ -38,10 +36,8 @@ export function addHmsPathMapping( wildcardTarget: string, ): void { const existing = paths[moduleName] - if (!existing) - paths[moduleName] = [filePath] - else if (!existing.includes(filePath)) - existing.push(filePath) + if (!existing) paths[moduleName] = [filePath] + else if (!existing.includes(filePath)) existing.push(filePath) paths[`${moduleName}/*`] = [wildcardTarget] }