diff --git a/packages/language-server/src/workspace.ts b/packages/language-server/src/workspace.ts index e573dab6..22e86583 100644 --- a/packages/language-server/src/workspace.ts +++ b/packages/language-server/src/workspace.ts @@ -1,9 +1,10 @@ import type { Connection, LanguageServer } from '@volar/language-server' +import type { CatalogsInfo, Engines } from 'npmx-language-core/types' import type { DependencyInfo, PackageManager, WorkspaceAdapter } from 'npmx-language-core/workspace' import type { ClientFeatures, IWorkspaceState } from 'npmx-language-service/types' import { access, realpath as fsRealpath, readFile } from 'node:fs/promises' import { CACHE_MAX_AGE_MAXIMUM, DEPENDENCY_FILE_GLOB, PACKAGE_JSON_BASENAME } from 'npmx-language-core/constants' -import { isDependencyFile, isPackageManifest } from 'npmx-language-core/utils' +import { isDependencyFile, isPackageManifest, normalizeCatalogName } from 'npmx-language-core/utils' import { WorkspaceContext } from 'npmx-language-core/workspace' import { DEFAULT_CLIENT_FEATURES } from 'npmx-language-service/types' import { defineCachedFunction } from 'ocache' @@ -163,7 +164,7 @@ export class WorkspaceState implements IWorkspaceState { return bestMatch } - async getWorkspaceContext(uriString: string): Promise { + async #getWorkspaceContext(uriString: string): Promise { const folderUri = this.#getWorkspaceFolderUri(uriString) if (!folderUri) return @@ -171,8 +172,54 @@ export class WorkspaceState implements IWorkspaceState { return await this.#getWorkspaceContextByFolder(folderUri) } + async findCatalogDependency(uriString: string, dependency: DependencyInfo) { + const ctx = await this.#getWorkspaceContext(uriString) + if (!ctx?.workspaceFilePath) + return + + const workspaceFileInfo = await ctx.loadWorkspaceFileInfo(ctx.workspaceFilePath) + const targetDependency = workspaceFileInfo?.dependencies.find((candidate) => + candidate.rawName === dependency.resolvedName + && candidate.categoryName != null + && dependency.categoryName != null + && normalizeCatalogName(candidate.categoryName) === normalizeCatalogName(dependency.categoryName), + ) + if (!targetDependency) + return + + return { dependency: targetDependency, path: ctx.workspaceFilePath } + } + + async findInstalledPackageManifestPath(uriString: string, packageName: string): Promise { + const ctx = await this.#getWorkspaceContext(uriString) + if (!ctx) + return + + const uri = URI.parse(uriString) + if (uri.scheme !== 'file' || !isPackageManifest(uri.path)) + return + + return ctx.findInstalledPackageManifestPath(uri.path, packageName) + } + + async getCatalogs(uriString: string): Promise { + return (await this.#getWorkspaceContext(uriString))?.getCatalogs() + } + + async getPackageEngines(uriString: string): Promise { + const ctx = await this.#getWorkspaceContext(uriString) + if (!ctx) + return + + const uri = URI.parse(uriString) + if (uri.scheme !== 'file' || !isPackageManifest(uri.path)) + return + + return (await ctx.loadPackageManifestInfo(uri.path))?.engines + } + async getResolvedDependencies(uriString: string): Promise { - const ctx = await this.getWorkspaceContext(uriString) + const ctx = await this.#getWorkspaceContext(uriString) if (!ctx) return @@ -194,7 +241,7 @@ export class WorkspaceState implements IWorkspaceState { } async getResolvedDependenciesForContainingPackage(uriString: string): Promise { - const ctx = await this.getWorkspaceContext(uriString) + const ctx = await this.#getWorkspaceContext(uriString) if (!ctx) return diff --git a/packages/language-service/src/plugins/catalog.ts b/packages/language-service/src/plugins/catalog.ts index e0ec1289..2c1bf9e6 100644 --- a/packages/language-service/src/plugins/catalog.ts +++ b/packages/language-service/src/plugins/catalog.ts @@ -1,7 +1,7 @@ import type { CompletionItemKind, CompletionList, LanguageServicePlugin, LanguageServicePluginInstance, LocationLink } from '@volar/language-service' import type { DependencyInfo } from 'npmx-language-core/workspace' import type { IWorkspaceState } from '../types' -import { isPackageManifest, normalizeCatalogName } from 'npmx-language-core/utils' +import { isPackageManifest } from 'npmx-language-core/utils' import { URI } from 'vscode-uri' import { getDocumentByUri, getResolvedDependencySpecAtOffset } from '../utils/document' @@ -33,13 +33,6 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { return getCatalogDependencyAtOffset(dependencies, offset) } - function matchesCatalogDependency(candidate: DependencyInfo, dependency: DependencyInfo): boolean { - return candidate.rawName === dependency.resolvedName - && candidate.categoryName != null - && dependency.categoryName != null - && normalizeCatalogName(candidate.categoryName) === normalizeCatalogName(dependency.categoryName) - } - return { name: 'npmx-catalog', capabilities: { @@ -61,11 +54,7 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { if (!dependency) return - const workspaceContext = await workspaceState.getWorkspaceContext(document.uri) - if (!workspaceContext) - return - - const catalogs = await workspaceContext.getCatalogs() + const catalogs = await workspaceState.getCatalogs(document.uri) if (!catalogs) return @@ -96,26 +85,16 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { if (!dependency) return - const workspaceContext = await workspaceState.getWorkspaceContext(document.uri) - if (!workspaceContext?.workspaceFilePath) - return - - const workspaceFileInfo = await workspaceContext.loadWorkspaceFileInfo(workspaceContext.workspaceFilePath) - if (!workspaceFileInfo) - return - - const targetDependency = workspaceFileInfo.dependencies.find((candidate) => - matchesCatalogDependency(candidate, dependency), - ) - if (!targetDependency) + const target = await workspaceState.findCatalogDependency(document.uri, dependency) + if (!target) return - const workspaceFileUri = dependencyFileUri.with({ path: workspaceContext.workspaceFilePath }) + const workspaceFileUri = dependencyFileUri.with({ path: target.path }) const workspaceDocument = await getDocumentByUri(context, workspaceFileUri) if (!workspaceDocument) return - const [targetStart, targetEnd] = targetDependency.specRange + const [targetStart, targetEnd] = target.dependency.specRange const originStart = document.positionAt(dependency.specRange[0]) const originEnd = document.positionAt(dependency.specRange[1]) diff --git a/packages/language-service/src/plugins/diagnostics/rules/__tests__/utils.ts b/packages/language-service/src/plugins/diagnostics/rules/__tests__/utils.ts index 5bbbe4ee..507e6bf9 100644 --- a/packages/language-service/src/plugins/diagnostics/rules/__tests__/utils.ts +++ b/packages/language-service/src/plugins/diagnostics/rules/__tests__/utils.ts @@ -16,7 +16,7 @@ interface CreateContextOptions { } export function createContext(options: CreateContextOptions): DiagnosticContext { - const { name, version, distTags = {}, versionsMeta = {}, category = 'dependencies' } = options + const { name, version, distTags = {}, versionsMeta = {}, engines, category = 'dependencies' } = options const { protocol, resolvedName, resolvedSpec, resolvedProtocol } = resolveDependencySpec(name, version) const pkg = { distTags, versionsMeta } as PackageInfo @@ -33,5 +33,26 @@ export function createContext(options: CreateContextOptions): DiagnosticContext resolvedVersion: async () => resolveExactVersion(pkg, resolvedSpec), packageInfo: async () => (pkg), } - return { uri: 'file:///package.json', dep, pkg } as DiagnosticContext + const workspace: DiagnosticContext['workspace'] = { + async findCatalogDependency() { + return undefined + }, + async findInstalledPackageManifestPath() { + return undefined + }, + async getCatalogs() { + return undefined + }, + getClientFeatures: () => ({ catalogInlayHints: true, iconStyle: 'emoji' }), + async getPackageEngines() { + return engines + }, + async getResolvedDependencies() { + return undefined + }, + async getResolvedDependenciesForContainingPackage() { + return undefined + }, + } + return { uri: 'file:///package.json', dep, pkg, workspace } } diff --git a/packages/language-service/src/plugins/diagnostics/rules/engine-mismatch.test.ts b/packages/language-service/src/plugins/diagnostics/rules/engine-mismatch.test.ts index 1e39e35b..e8607795 100644 --- a/packages/language-service/src/plugins/diagnostics/rules/engine-mismatch.test.ts +++ b/packages/language-service/src/plugins/diagnostics/rules/engine-mismatch.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import { resolveEngineMismatches } from './engine-mismatch' +import { createContext } from './__tests__/utils' +import { checkEngineMismatch, resolveEngineMismatches } from './engine-mismatch' describe('resolveEngineMismatches', () => { it('should flag when engine ranges do not overlap', () => { @@ -62,3 +63,19 @@ describe('resolveEngineMismatches', () => { )).toEqual([]) }) }) + +describe('checkEngineMismatch', () => { + it('reads package engines through the workspace interface', async () => { + await expect(checkEngineMismatch( + createContext({ + name: 'foo', + version: '1.0.0', + engines: { node: '>=20' }, + versionsMeta: { + '1.0.0': { engines: { node: '>=22' } }, + }, + }), + [], + )).resolves.toMatchObject({ code: 'engine-mismatch' }) + }) +}) diff --git a/packages/language-service/src/plugins/diagnostics/rules/engine-mismatch.ts b/packages/language-service/src/plugins/diagnostics/rules/engine-mismatch.ts index f3bdc7bf..aa704008 100644 --- a/packages/language-service/src/plugins/diagnostics/rules/engine-mismatch.ts +++ b/packages/language-service/src/plugins/diagnostics/rules/engine-mismatch.ts @@ -2,9 +2,8 @@ import type { DiagnosticSeverity } from '@volar/language-service' import type { Engines } from 'npmx-language-core/types' import type { DiagnosticRule } from '../types' import { npmxPackageUrl } from 'npmx-language-core/links' -import { formatPackageId, isPackageManifest } from 'npmx-language-core/utils' +import { formatPackageId } from 'npmx-language-core/utils' import { isRangeSubset, parseRange, rangesIntersect } from 'verkit' -import { URI } from 'vscode-uri' interface EngineMismatch { engine: string @@ -46,10 +45,6 @@ export function resolveEngineMismatches( } export const checkEngineMismatch: DiagnosticRule = async ({ uri, dep, pkg, workspace }) => { - const path = URI.parse(uri).path - - if (!isPackageManifest(path)) - return if (dep.category !== 'dependencies') return @@ -57,8 +52,7 @@ export const checkEngineMismatch: DiagnosticRule = async ({ uri, dep, pkg, works if (!resolvedVersion) return - const wsCtx = await workspace.getWorkspaceContext(uri) - const engines = (await wsCtx?.loadPackageManifestInfo(path))?.engines + const engines = await workspace.getPackageEngines(uri) if (!engines) return diff --git a/packages/language-service/src/plugins/installed-package-definition.test.ts b/packages/language-service/src/plugins/installed-package-definition.test.ts index e8e33b5c..fca81788 100644 --- a/packages/language-service/src/plugins/installed-package-definition.test.ts +++ b/packages/language-service/src/plugins/installed-package-definition.test.ts @@ -3,6 +3,7 @@ import type { IWorkspaceState } from '../types' import { WorkspaceContext } from 'npmx-language-core/workspace' import { describe, expect, it } from 'vitest' import { TextDocument } from 'vscode-languageserver-textdocument' +import { URI } from 'vscode-uri' import { createDependencyInfo } from '../test-utils/dependency' import { DEFAULT_CLIENT_FEATURES } from '../types' import { provideInstalledPackageDefinition } from './installed-package-definition' @@ -26,9 +27,18 @@ async function createWorkspaceState( const workspaceContext = await WorkspaceContext.create('/repo', adapter) return { + async findCatalogDependency() { + return undefined + }, + async findInstalledPackageManifestPath(uri, packageName) { + return workspaceContext.findInstalledPackageManifestPath(URI.parse(uri).path, packageName) + }, + async getCatalogs() { + return undefined + }, getClientFeatures: () => DEFAULT_CLIENT_FEATURES, - async getWorkspaceContext() { - return workspaceContext + async getPackageEngines() { + return undefined }, async getResolvedDependencies() { return dependencies diff --git a/packages/language-service/src/plugins/installed-package-definition.ts b/packages/language-service/src/plugins/installed-package-definition.ts index 64b81a9a..3c2ebe73 100644 --- a/packages/language-service/src/plugins/installed-package-definition.ts +++ b/packages/language-service/src/plugins/installed-package-definition.ts @@ -25,12 +25,8 @@ export async function provideInstalledPackageDefinition( if (!dependency) return - const workspaceContext = await workspaceState.getWorkspaceContext(document.uri) - if (!workspaceContext) - return - - const targetPath = await workspaceContext.findInstalledPackageManifestPath( - packageManifestUri.path, + const targetPath = await workspaceState.findInstalledPackageManifestPath( + document.uri, dependency.rawName, ) if (!targetPath) diff --git a/packages/language-service/src/types.ts b/packages/language-service/src/types.ts index f1e2d1a4..afe6087a 100644 --- a/packages/language-service/src/types.ts +++ b/packages/language-service/src/types.ts @@ -1,4 +1,5 @@ -import type { DependencyInfo, WorkspaceContext } from 'npmx-language-core/workspace' +import type { CatalogsInfo, Engines } from 'npmx-language-core/types' +import type { DependencyInfo } from 'npmx-language-core/workspace' export type IconStyle = 'codicon' | 'emoji' @@ -14,7 +15,13 @@ export const DEFAULT_CLIENT_FEATURES: ClientFeatures = { export interface IWorkspaceState { getClientFeatures: () => ClientFeatures - getWorkspaceContext: (uri: string) => Promise + getCatalogs: (uri: string) => Promise + findCatalogDependency: (uri: string, dependency: DependencyInfo) => Promise<{ + dependency: DependencyInfo + path: string + } | undefined> + getPackageEngines: (uri: string) => Promise getResolvedDependencies: (uri: string) => Promise getResolvedDependenciesForContainingPackage: (uri: string) => Promise + findInstalledPackageManifestPath: (uri: string, packageName: string) => Promise }