Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/uidesignkit-titlebar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@arkts/language-server": patch
---

fix: resolve UIDesignKit exports such as `.titleBar` after `hmsPath` is set
64 changes: 41 additions & 23 deletions packages/language-server/src/classes/config-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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<string[]> {
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<string[]> {
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<string[]>): Promise<void> {
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<import('typescript').MapLike<string[]>> {
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<string[]> = {}
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) {
Expand All @@ -197,11 +210,16 @@ export class ConfigResolver {
}

async getPaths(): Promise<ets.MapLike<string[]>> {
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(),
Expand Down
48 changes: 48 additions & 0 deletions packages/language-server/src/utils/hms-paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* 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<string>([moduleName])
if (moduleName.includes('/')) names.add(moduleName.replaceAll('/', '.'))
return [...names]
}

export function addHmsPathMapping(
paths: Record<string, string[]>,
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}/*`)
}
53 changes: 53 additions & 0 deletions packages/language-server/test/hms-paths.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string[]> = {}
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/*',
])
})
})