From 8a4ce805ac9726e4a662fd12ff2374292861507d Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Sat, 21 Mar 2026 13:16:29 +0800 Subject: [PATCH 1/2] refactor: extract more vscode-independent logic to `language-core` --- extensions/vscode/package.json | 3 - .../vscode/src/commands/add-to-ignore.ts | 2 +- .../vscode/src/commands/open-file-in-npmx.ts | 2 +- .../vscode/src/commands/open-in-browser.ts | 2 +- .../src/composables/workspace-context.ts | 12 ++-- extensions/vscode/src/core/workspace.ts | 16 ++--- extensions/vscode/src/providers/decorators.ts | 4 +- .../vscode/src/providers/diagnostics/index.ts | 4 +- .../diagnostics/rules/__tests__/utils.ts | 7 +- .../diagnostics/rules/deprecation.ts | 5 +- .../providers/diagnostics/rules/dist-tag.ts | 2 +- .../diagnostics/rules/engine-mismatch.ts | 9 ++- .../diagnostics/rules/replacement.ts | 4 +- .../diagnostics/rules/upgrade.test.ts | 2 +- .../providers/diagnostics/rules/upgrade.ts | 6 +- .../diagnostics/rules/vulnerability.ts | 9 ++- .../src/providers/document-link/npmx.ts | 2 +- extensions/vscode/src/providers/hover/npmx.ts | 2 +- .../vscode/src/providers/hover/resolve.ts | 5 +- extensions/vscode/src/utils/constants.ts | 5 -- extensions/vscode/src/utils/file.ts | 36 +--------- extensions/vscode/src/utils/package.test.ts | 44 ------------- extensions/vscode/src/utils/package.ts | 65 ------------------ extensions/vscode/tests/__setup__/msw.ts | 2 +- extensions/vscode/tsdown.config.ts | 2 - packages/language-core/package.json | 13 ++++ .../language-core}/src/api/package.ts | 16 ++--- .../language-core}/src/api/replacement.ts | 7 +- .../language-core}/src/api/vulnerability.ts | 8 +-- packages/language-core/src/constants.ts | 5 ++ .../language-core/src}/links.ts | 2 +- packages/language-core/src/types.ts | 4 +- .../language-core}/src/utils/batch.test.ts | 0 .../language-core}/src/utils/batch.ts | 0 packages/language-core/src/utils/file.ts | 21 ++++++ .../language-core/src/utils/general.ts | 0 .../language-core}/src/utils/ignore.test.ts | 0 .../language-core}/src/utils/ignore.ts | 2 +- packages/language-core/src/utils/index.ts | 4 ++ .../language-core/src/utils/package.test.ts | 44 ++++++++++++- packages/language-core/src/utils/package.ts | 66 +++++++++++++++++++ packages/language-core/tsdown.config.ts | 5 ++ pnpm-lock.yaml | 25 ++++--- 43 files changed, 234 insertions(+), 240 deletions(-) delete mode 100644 extensions/vscode/src/utils/package.test.ts delete mode 100644 extensions/vscode/src/utils/package.ts rename {extensions/vscode => packages/language-core}/src/api/package.ts (76%) rename {extensions/vscode => packages/language-core}/src/api/replacement.ts (68%) rename {extensions/vscode => packages/language-core}/src/api/vulnerability.ts (88%) rename {extensions/vscode/src/utils => packages/language-core/src}/links.ts (95%) rename {extensions/vscode => packages/language-core}/src/utils/batch.test.ts (100%) rename {extensions/vscode => packages/language-core}/src/utils/batch.ts (100%) create mode 100644 packages/language-core/src/utils/file.ts rename extensions/vscode/src/utils/shared.ts => packages/language-core/src/utils/general.ts (100%) rename {extensions/vscode => packages/language-core}/src/utils/ignore.test.ts (100%) rename {extensions/vscode => packages/language-core}/src/utils/ignore.ts (84%) diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 7aa3793..be855e1 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -205,11 +205,8 @@ }, "devDependencies": { "@types/vscode": "1.101.0", - "fast-npm-meta": "catalog:inline", "npmx-language-core": "workspace:*", "ocache": "catalog:inline", - "ofetch": "catalog:inline", - "pathe": "catalog:inline", "perfect-debounce": "catalog:inline", "reactive-vscode": "catalog:inline", "semver": "catalog:inline", diff --git a/extensions/vscode/src/commands/add-to-ignore.ts b/extensions/vscode/src/commands/add-to-ignore.ts index 58526ec..4a4f65c 100644 --- a/extensions/vscode/src/commands/add-to-ignore.ts +++ b/extensions/vscode/src/commands/add-to-ignore.ts @@ -1,6 +1,6 @@ import type { ConfigurationTarget } from 'vscode' import { scopedConfigs } from '#shared/meta' -import { checkIgnored } from '#utils/ignore' +import { checkIgnored } from 'npmx-language-core/utils' import { workspace } from 'vscode' export async function addToIgnore(scope: string, name: string, target: ConfigurationTarget) { diff --git a/extensions/vscode/src/commands/open-file-in-npmx.ts b/extensions/vscode/src/commands/open-file-in-npmx.ts index d6e027b..edec294 100644 --- a/extensions/vscode/src/commands/open-file-in-npmx.ts +++ b/extensions/vscode/src/commands/open-file-in-npmx.ts @@ -1,7 +1,7 @@ import { logger } from '#state' import { readPackageManifest } from '#utils/file' -import { npmxFileUrl } from '#utils/links' import { PACKAGE_JSON_BASENAME } from 'npmx-language-core/constants' +import { npmxFileUrl } from 'npmx-language-core/links' import { env, Uri, window } from 'vscode' import { findUp } from 'vscode-find-up' diff --git a/extensions/vscode/src/commands/open-in-browser.ts b/extensions/vscode/src/commands/open-in-browser.ts index 62c5ddd..5457082 100644 --- a/extensions/vscode/src/commands/open-in-browser.ts +++ b/extensions/vscode/src/commands/open-in-browser.ts @@ -1,4 +1,4 @@ -import { NPMX_DEV } from '#utils/constants' +import { NPMX_DEV } from 'npmx-language-core/constants' import { env, Uri } from 'vscode' export function openInBrowser() { diff --git a/extensions/vscode/src/composables/workspace-context.ts b/extensions/vscode/src/composables/workspace-context.ts index 39ef6ba..01f5dc3 100644 --- a/extensions/vscode/src/composables/workspace-context.ts +++ b/extensions/vscode/src/composables/workspace-context.ts @@ -2,7 +2,8 @@ import type { Uri } from 'vscode' import { deleteWorkspaceContextCache, getWorkspaceContext } from '#core/workspace' import { logger } from '#state' import { SUPPORTED_DOCUMENT_PATTERN } from '#utils/constants' -import { isSupportedDependencyDocument, isWorkspaceLevelFile } from '#utils/file' +import { PACKAGE_JSON_BASENAME } from 'npmx-language-core/constants' +import { isDependencyFile, isWorkspaceFile } from 'npmx-language-core/utils' import { useDisposable, useFileSystemWatcher } from 'reactive-vscode' import { window, workspace } from 'vscode' @@ -15,7 +16,7 @@ export function useWorkspaceContext() { })) async function deleteCacheByUri(uri: Uri, reload = true) { - if (!isSupportedDependencyDocument(uri)) + if (!isDependencyFile(uri.path)) return const ctx = await getWorkspaceContext(uri) @@ -24,8 +25,11 @@ export function useWorkspaceContext() { ctx.invalidateDependencyInfo(uri) logger.info(`[workspace-context] invalidate dependencies cache: ${uri.path}`) - if (reload && isWorkspaceLevelFile(uri)) { - await ctx.loadWorkspace() + if (reload) { + const folderPath = ctx!.folder.uri.path + const isRoot = uri.path === `${folderPath}/${PACKAGE_JSON_BASENAME}` + if (isRoot || isWorkspaceFile(uri.path)) + await ctx.loadWorkspace() } } diff --git a/extensions/vscode/src/core/workspace.ts b/extensions/vscode/src/core/workspace.ts index 6b3daa5..9bb7254 100644 --- a/extensions/vscode/src/core/workspace.ts +++ b/extensions/vscode/src/core/workspace.ts @@ -1,5 +1,5 @@ -import type { PackageInfo } from '#api/package' import type { PackageManager } from '#shared/types' +import type { PackageInfo } from 'npmx-language-core/api/package' import type { CatalogsInfo, ExtractedDependencyInfo, @@ -9,15 +9,13 @@ import type { } from 'npmx-language-core/types' import type { CacheOptions } from 'ocache' import type { WorkspaceFolder } from 'vscode' -import { getPackageInfo } from '#api/package' import { logger } from '#state' import { isOffsetInRange } from '#utils/ast' -import { getDocumentText, isPackageManifestPath, isWorkspaceFilePath } from '#utils/file' -import { resolveExactVersion } from '#utils/package' -import { lazyInit } from '#utils/shared' +import { getDocumentText } from '#utils/file' +import { getPackageInfo } from 'npmx-language-core/api/package' import { PNPM_WORKSPACE_BASENAME, YARN_WORKSPACE_BASENAME } from 'npmx-language-core/constants' import { getExtractor } from 'npmx-language-core/extractors' -import { resolveDependencySpec } from 'npmx-language-core/utils' +import { isPackageManifest, isWorkspaceFile, lazyInit, resolveDependencySpec, resolveExactVersion } from 'npmx-language-core/utils' import { defineCachedFunction } from 'ocache' import { commands, Uri, window, workspace } from 'vscode' import { accessOk } from 'vscode-find-up' @@ -133,7 +131,7 @@ class WorkspaceContext { [Uri] >(async (uri) => { const path = uri.path - if (!isPackageManifestPath(path)) + if (!isPackageManifest(path)) return logger.info(`[workspace-context] load package manifest info: ${path}`) @@ -161,7 +159,7 @@ class WorkspaceContext { [Uri] >(async (uri) => { const path = uri.path - if (!isWorkspaceFilePath(path)) + if (!isWorkspaceFile(path)) return logger.info(`[workspace-context] load workspace catalog info: ${path}`) @@ -217,7 +215,7 @@ export async function getResolvedDependencies(uri: Uri): Promise { const document = editor.document - if (!isPackageManifestPath(document.uri.path)) + if (!isPackageManifest(document.uri.path)) return [] logger.info(`[decorators] updating ${document.uri.path}`) diff --git a/extensions/vscode/src/providers/diagnostics/index.ts b/extensions/vscode/src/providers/diagnostics/index.ts index e4190a4..3e991b4 100644 --- a/extensions/vscode/src/providers/diagnostics/index.ts +++ b/extensions/vscode/src/providers/diagnostics/index.ts @@ -7,7 +7,7 @@ import { displayName } from '#shared/meta' import { config, logger } from '#state' import { offsetRangeToRange } from '#utils/ast' import { SUPPORTED_DOCUMENT_PATTERN } from '#utils/constants' -import { isSupportedDependencyDocument } from '#utils/file' +import { isDependencyFile } from 'npmx-language-core/utils' import { debounce } from 'perfect-debounce' import { computed, nextTick, useActiveTextEditor, useDisposable, useDocumentText, useFileSystemWatcher, watch } from 'reactive-vscode' import { languages, TabInputText, window, workspace } from 'vscode' @@ -127,7 +127,7 @@ export function useDiagnostics() { return const document = activeEditor.value.document - if (!isSupportedDependencyDocument(document)) + if (!isDependencyFile(document.uri.path)) return collectDiagnostics(document) diff --git a/extensions/vscode/src/providers/diagnostics/rules/__tests__/utils.ts b/extensions/vscode/src/providers/diagnostics/rules/__tests__/utils.ts index 02a2f6d..9455df6 100644 --- a/extensions/vscode/src/providers/diagnostics/rules/__tests__/utils.ts +++ b/extensions/vscode/src/providers/diagnostics/rules/__tests__/utils.ts @@ -1,8 +1,7 @@ -import type { PackageInfo } from '#api/package' -import type { Engines } from 'fast-npm-meta' +import type { PackageInfo } from 'npmx-language-core/api/package' +import type { Engines } from 'npmx-language-core/types' import type { DiagnosticContext } from '../..' -import { resolveExactVersion } from '#utils/package' -import { resolveDependencySpec } from 'npmx-language-core/utils' +import { resolveDependencySpec, resolveExactVersion } from 'npmx-language-core/utils' import { Uri } from 'vscode' interface CreateContextOptions { diff --git a/extensions/vscode/src/providers/diagnostics/rules/deprecation.ts b/extensions/vscode/src/providers/diagnostics/rules/deprecation.ts index 5194de2..bae8a60 100644 --- a/extensions/vscode/src/providers/diagnostics/rules/deprecation.ts +++ b/extensions/vscode/src/providers/diagnostics/rules/deprecation.ts @@ -1,8 +1,7 @@ import type { DiagnosticRule } from '..' import { config } from '#state' -import { checkIgnored } from '#utils/ignore' -import { npmxPackageUrl } from '#utils/links' -import { formatPackageId } from 'npmx-language-core/utils' +import { npmxPackageUrl } from 'npmx-language-core/links' +import { checkIgnored, formatPackageId } from 'npmx-language-core/utils' import { DiagnosticSeverity, DiagnosticTag, Uri } from 'vscode' export const checkDeprecation: DiagnosticRule = async ({ dep, pkg }) => { diff --git a/extensions/vscode/src/providers/diagnostics/rules/dist-tag.ts b/extensions/vscode/src/providers/diagnostics/rules/dist-tag.ts index eaf1274..52b4b41 100644 --- a/extensions/vscode/src/providers/diagnostics/rules/dist-tag.ts +++ b/extensions/vscode/src/providers/diagnostics/rules/dist-tag.ts @@ -1,5 +1,5 @@ import type { DiagnosticRule } from '..' -import { npmxPackageUrl } from '#utils/links' +import { npmxPackageUrl } from 'npmx-language-core/links' import { DiagnosticSeverity, Uri } from 'vscode' export const checkDistTag: DiagnosticRule = async ({ dep, pkg }) => { diff --git a/extensions/vscode/src/providers/diagnostics/rules/engine-mismatch.ts b/extensions/vscode/src/providers/diagnostics/rules/engine-mismatch.ts index a739388..875b77a 100644 --- a/extensions/vscode/src/providers/diagnostics/rules/engine-mismatch.ts +++ b/extensions/vscode/src/providers/diagnostics/rules/engine-mismatch.ts @@ -1,9 +1,8 @@ -import type { Engines } from 'fast-npm-meta' +import type { Engines } from 'npmx-language-core/types' import type { DiagnosticRule } from '..' import { getWorkspaceContext } from '#core/workspace' -import { isPackageManifestPath } from '#utils/file' -import { npmxPackageUrl } from '#utils/links' -import { formatPackageId } from 'npmx-language-core/utils' +import { npmxPackageUrl } from 'npmx-language-core/links' +import { formatPackageId, isPackageManifest } from 'npmx-language-core/utils' import Range from 'semver/classes/range' import intersects from 'semver/ranges/intersects' import subset from 'semver/ranges/subset' @@ -49,7 +48,7 @@ export function resolveEngineMismatches( } export const checkEngineMismatch: DiagnosticRule = async ({ uri, dep, pkg }) => { - if (!isPackageManifestPath(uri.path)) + if (!isPackageManifest(uri.path)) return if (dep.category !== 'dependencies') return diff --git a/extensions/vscode/src/providers/diagnostics/rules/replacement.ts b/extensions/vscode/src/providers/diagnostics/rules/replacement.ts index 971b09b..29e5178 100644 --- a/extensions/vscode/src/providers/diagnostics/rules/replacement.ts +++ b/extensions/vscode/src/providers/diagnostics/rules/replacement.ts @@ -1,8 +1,8 @@ import type { ModuleReplacement } from 'module-replacements' import type { DiagnosticRule } from '..' -import { getReplacement } from '#api/replacement' import { config } from '#state' -import { checkIgnored } from '#utils/ignore' +import { getReplacement } from 'npmx-language-core/api/replacement' +import { checkIgnored } from 'npmx-language-core/utils' import { DiagnosticSeverity, Uri } from 'vscode' function getMdnUrl(path: string): string { diff --git a/extensions/vscode/src/providers/diagnostics/rules/upgrade.test.ts b/extensions/vscode/src/providers/diagnostics/rules/upgrade.test.ts index 0e7d029..ca80048 100644 --- a/extensions/vscode/src/providers/diagnostics/rules/upgrade.test.ts +++ b/extensions/vscode/src/providers/diagnostics/rules/upgrade.test.ts @@ -1,5 +1,5 @@ -import type { PackageInfo } from '#api/package' import type { DependencyInfo } from '#core/workspace' +import type { PackageInfo } from 'npmx-language-core/api/package' import { describe, expect, it } from 'vitest' import { createContext } from './__tests__/utils' import { resolveUpgrade } from './upgrade' diff --git a/extensions/vscode/src/providers/diagnostics/rules/upgrade.ts b/extensions/vscode/src/providers/diagnostics/rules/upgrade.ts index 57a8fd1..112c9ab 100644 --- a/extensions/vscode/src/providers/diagnostics/rules/upgrade.ts +++ b/extensions/vscode/src/providers/diagnostics/rules/upgrade.ts @@ -1,11 +1,11 @@ -import type { PackageInfo } from '#api/package' import type { DependencyInfo } from '#core/workspace' +import type { PackageInfo } from 'npmx-language-core/api/package' import type { OffsetRange } from 'npmx-language-core/types' import type { DiagnosticRule, RangeDiagnosticInfo } from '..' import { config } from '#state' -import { checkIgnored } from '#utils/ignore' -import { npmxPackageUrl } from '#utils/links' import { formatUpgradeVersion } from '#utils/version' +import { npmxPackageUrl } from 'npmx-language-core/links' +import { checkIgnored } from 'npmx-language-core/utils' import gt from 'semver/functions/gt' import lte from 'semver/functions/lte' import prerelease from 'semver/functions/prerelease' diff --git a/extensions/vscode/src/providers/diagnostics/rules/vulnerability.ts b/extensions/vscode/src/providers/diagnostics/rules/vulnerability.ts index a174276..60b2b90 100644 --- a/extensions/vscode/src/providers/diagnostics/rules/vulnerability.ts +++ b/extensions/vscode/src/providers/diagnostics/rules/vulnerability.ts @@ -1,11 +1,10 @@ -import type { OsvSeverityLevel, PackageVulnerabilityInfo } from '#api/vulnerability' +import type { OsvSeverityLevel, PackageVulnerabilityInfo } from 'npmx-language-core/api/vulnerability' import type { DiagnosticRule } from '..' -import { getVulnerability, SEVERITY_LEVELS } from '#api/vulnerability' import { config } from '#state' -import { checkIgnored } from '#utils/ignore' -import { npmxPackageUrl } from '#utils/links' import { formatUpgradeVersion } from '#utils/version' -import { formatPackageId } from 'npmx-language-core/utils' +import { getVulnerability, SEVERITY_LEVELS } from 'npmx-language-core/api/vulnerability' +import { npmxPackageUrl } from 'npmx-language-core/links' +import { checkIgnored, formatPackageId } from 'npmx-language-core/utils' import lt from 'semver/functions/lt' import { DiagnosticSeverity, Uri } from 'vscode' diff --git a/extensions/vscode/src/providers/document-link/npmx.ts b/extensions/vscode/src/providers/document-link/npmx.ts index 0802a22..3904f84 100644 --- a/extensions/vscode/src/providers/document-link/npmx.ts +++ b/extensions/vscode/src/providers/document-link/npmx.ts @@ -2,7 +2,7 @@ import type { DocumentLink, DocumentLinkProvider, TextDocument } from 'vscode' import { getResolvedDependencies } from '#core/workspace' import { config, logger } from '#state' import { offsetRangeToRange } from '#utils/ast' -import { npmxPackageUrl } from '#utils/links' +import { npmxPackageUrl } from 'npmx-language-core/links' import { Uri, DocumentLink as VscodeDocumentLink } from 'vscode' export class NpmxDocumentLinkProvider implements DocumentLinkProvider { diff --git a/extensions/vscode/src/providers/hover/npmx.ts b/extensions/vscode/src/providers/hover/npmx.ts index 86935ab..684c62c 100644 --- a/extensions/vscode/src/providers/hover/npmx.ts +++ b/extensions/vscode/src/providers/hover/npmx.ts @@ -1,5 +1,5 @@ import type { HoverProvider, Position, TextDocument } from 'vscode' -import { jsrPackageUrl, npmxDocsUrl, npmxPackageUrl } from '#utils/links' +import { jsrPackageUrl, npmxDocsUrl, npmxPackageUrl } from 'npmx-language-core/links' import { Hover, MarkdownString } from 'vscode' import { resolveHoverDependency } from './resolve' diff --git a/extensions/vscode/src/providers/hover/resolve.ts b/extensions/vscode/src/providers/hover/resolve.ts index bab0bfd..45edfcd 100644 --- a/extensions/vscode/src/providers/hover/resolve.ts +++ b/extensions/vscode/src/providers/hover/resolve.ts @@ -1,9 +1,8 @@ import type { DependencyInfo } from '#core/workspace' import type { Position, TextDocument } from 'vscode' import { getResolvedDependencies, getResolvedDependencyByOffset } from '#core/workspace' -import { isSupportedDependencyDocument } from '#utils/file' import { PACKAGE_JSON_BASENAME } from 'npmx-language-core/constants' -import { getImportSpecifierInLine } from 'npmx-language-core/utils' +import { getImportSpecifierInLine, isDependencyFile } from 'npmx-language-core/utils' import { findUp } from 'vscode-find-up' export async function resolveHoverDependency( @@ -12,7 +11,7 @@ export async function resolveHoverDependency( ): Promise { const offset = document.offsetAt(position) - if (isSupportedDependencyDocument(document)) + if (isDependencyFile(document.uri.path)) return await getResolvedDependencyByOffset(document.uri, offset) if (document.uri.scheme !== 'file') diff --git a/extensions/vscode/src/utils/constants.ts b/extensions/vscode/src/utils/constants.ts index 5f95e63..eaa62f0 100644 --- a/extensions/vscode/src/utils/constants.ts +++ b/extensions/vscode/src/utils/constants.ts @@ -4,8 +4,3 @@ export const PACKAGE_JSON_PATTERN = `**/${PACKAGE_JSON_BASENAME}` export const SUPPORTED_DOCUMENT_PATTERN = `**/{${PACKAGE_JSON_BASENAME},${PNPM_WORKSPACE_BASENAME},${YARN_WORKSPACE_BASENAME}}` export const PRERELEASE_PATTERN = /-.+/ - -export const CACHE_MAX_AGE_ONE_DAY = 60 * 60 * 24 - -export const NPMX_DEV = 'https://npmx.dev' -export const NPMX_DEV_API = `${NPMX_DEV}/api` diff --git a/extensions/vscode/src/utils/file.ts b/extensions/vscode/src/utils/file.ts index be670b5..c1cb5fe 100644 --- a/extensions/vscode/src/utils/file.ts +++ b/extensions/vscode/src/utils/file.ts @@ -1,7 +1,5 @@ import type { PackageManifestInfo } from 'npmx-language-core/types' -import type { TextDocument, Uri } from 'vscode' -import { PACKAGE_JSON_BASENAME, PNPM_WORKSPACE_BASENAME, YARN_WORKSPACE_BASENAME } from 'npmx-language-core/constants' -import { basename } from 'pathe' +import type { Uri } from 'vscode' import { workspace } from 'vscode' export async function getDocumentText(uri: Uri) { @@ -9,38 +7,6 @@ export async function getDocumentText(uri: Uri) { return document.getText() } -const SUPPORTED_BASENAMES = new Set([ - PACKAGE_JSON_BASENAME, - PNPM_WORKSPACE_BASENAME, - YARN_WORKSPACE_BASENAME, -]) - -export function isSupportedDependencyDocument(documentOrUri: TextDocument | Uri): boolean { - const path = 'uri' in documentOrUri ? documentOrUri.uri.path : documentOrUri.path - return SUPPORTED_BASENAMES.has(basename(path)) -} - -export function isPackageManifestPath(path: string): path is `${string}/${typeof PACKAGE_JSON_BASENAME}` { - return path.endsWith(`/${PACKAGE_JSON_BASENAME}`) -} - -export function isWorkspaceFilePath(path: string): path is `${string}/${typeof PNPM_WORKSPACE_BASENAME}` | `${string}/${typeof YARN_WORKSPACE_BASENAME}` { - return path.endsWith(`/${PNPM_WORKSPACE_BASENAME}`) - || path.endsWith(`/${YARN_WORKSPACE_BASENAME}`) -} - -export function isRootPackageJson(uri: Uri): boolean { - const folder = workspace.getWorkspaceFolder(uri) - if (!folder) - return false - - return uri.path === `${folder.uri.path}/${PACKAGE_JSON_BASENAME}` -} - -export function isWorkspaceLevelFile(uri: Uri): boolean { - return isWorkspaceFilePath(uri.path) || isRootPackageJson(uri) -} - /** * Reads and parses a `package.json` file. * diff --git a/extensions/vscode/src/utils/package.test.ts b/extensions/vscode/src/utils/package.test.ts deleted file mode 100644 index 5954c0c..0000000 --- a/extensions/vscode/src/utils/package.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { PackageInfo } from '#api/package' -import { describe, expect, it } from 'vitest' -import { encodePackageName, resolveExactVersion } from './package' - -describe('encodePackageName', () => { - it('should encode regular package name', () => { - expect(encodePackageName('lodash')).toBe('lodash') - }) - - it('should encode scoped package name', () => { - expect(encodePackageName('@vue/core')).toBe('@vue%2Fcore') - }) -}) - -describe('resolveExactVersion', () => { - it.each([ - ['', '4.10.0'], - ['*', '4.10.0'], - ['^3.0.0', '3.1.0'], - ['^4.0.0', '4.10.0'], - ['^5.0.0', null], - ['latest', '4.10.0'], - ['next', '4.11.0-beta.1'], - ['beta', null], - ])('should resolve $0 to $1', (spec, version) => { - const pkg = { - distTags: { - latest: '4.10.0', - next: '4.11.0-beta.1', - }, - versionsMeta: { - '3.0.0': {}, - '3.1.0': {}, - '4.10.0': {}, - '4.10.1': { - deprecated: 'Unplanned Release', - }, - '4.11.0-beta.1': {}, - }, - } as unknown as PackageInfo - - expect(resolveExactVersion(pkg, spec)).toBe(version) - }) -}) diff --git a/extensions/vscode/src/utils/package.ts b/extensions/vscode/src/utils/package.ts deleted file mode 100644 index dea3349..0000000 --- a/extensions/vscode/src/utils/package.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { PackageInfo } from '#api/package' -import Range from 'semver/classes/range' -import gt from 'semver/functions/gt' -import lte from 'semver/functions/lte' -import satisfies from 'semver/functions/satisfies' - -/** - * Encode a package name for use in npm registry URLs. - * Handles scoped packages (e.g., @scope/name -> @scope%2Fname). - */ -export function encodePackageName(name: string): string { - if (name.startsWith('@')) { - return `@${encodeURIComponent(name.slice(1))}` - } - return encodeURIComponent(name) -} - -/** - * Resolve the maximum version satisfying the given range, capped by the `latest` dist-tag when possible. - * - * This first reads the `latest` tag, then selects the highest version that satisfies the range - * without exceeding that `latest` version. - * - * Inspired by: - * https://github.com/antfu-collective/taze/blob/fed751d777620ddb0a0e77a05ea1412f6332d043/src/utils/versions.ts#L66-L104 - */ -function getMaxSatisfying(versions: string[], current: string, tags: PackageInfo['distTags']) { - let version: string | null = null - - try { - const range = new Range(current) - - let maxVersion: string | null = tags.latest - if (!satisfies(maxVersion, range)) - maxVersion = null - - for (const ver of versions) { - if (!satisfies(ver, range)) - continue - - if (!maxVersion || lte(ver, maxVersion)) { - if (!version || gt(ver, version)) { - version = ver - } - } - } - return version - } catch { - return null - } -} - -export function resolveExactVersion(pkg: PackageInfo, version: string) { - if (version === '' || version === '*') - version = 'latest' - - if (Object.hasOwn(pkg.distTags, version)) - return pkg.distTags[version]! - - const versions = Object.keys(pkg.versionsMeta) - if (versions.length === 0) - return null - - return getMaxSatisfying(versions, version, pkg.distTags) -} diff --git a/extensions/vscode/tests/__setup__/msw.ts b/extensions/vscode/tests/__setup__/msw.ts index 43e6912..02a9529 100644 --- a/extensions/vscode/tests/__setup__/msw.ts +++ b/extensions/vscode/tests/__setup__/msw.ts @@ -1,7 +1,7 @@ -import { NPMX_DEV_API } from '#utils/constants' import { all } from 'module-replacements' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' +import { NPMX_DEV_API } from 'npmx-language-core/constants' import { afterAll, afterEach, beforeAll } from 'vitest' const replacementsByName = new Map( diff --git a/extensions/vscode/tsdown.config.ts b/extensions/vscode/tsdown.config.ts index db46a2b..933966f 100644 --- a/extensions/vscode/tsdown.config.ts +++ b/extensions/vscode/tsdown.config.ts @@ -9,11 +9,9 @@ export default defineConfig({ neverBundle: ['vscode'], /// keep-sorted onlyBundle: [ - 'fast-npm-meta', 'ocache', 'ofetch', 'ohash', - 'pathe', 'perfect-debounce', 'semver', 'vscode-find-up', diff --git a/packages/language-core/package.json b/packages/language-core/package.json index 64e9785..e09319e 100644 --- a/packages/language-core/package.json +++ b/packages/language-core/package.json @@ -11,8 +11,12 @@ }, "sideEffects": false, "exports": { + "./api/package": "./dist/api/package.js", + "./api/replacement": "./dist/api/replacement.js", + "./api/vulnerability": "./dist/api/vulnerability.js", "./constants": "./dist/constants.js", "./extractors": "./dist/extractors/index.js", + "./links": "./dist/links.js", "./types": "./dist/types.js", "./utils": "./dist/utils/index.js", "./package.json": "./package.json" @@ -24,12 +28,21 @@ "dev": "tsdown --watch", "build": "tsdown" }, + "dependencies": { + "ocache": "catalog:inline", + "ofetch": "catalog:inline", + "semver": "catalog:inline" + }, "devDependencies": { + "fast-npm-meta": "catalog:inline", "jsonc-parser": "catalog:inline", + "module-replacements": "catalog:test", "pathe": "catalog:inline", "yaml": "catalog:inline" }, "inlinedDependencies": { + "fast-npm-meta": "1.4.2", + "module-replacements": "2.11.0", "jsonc-parser": "3.3.1", "yaml": "2.8.2", "pathe": "2.0.3" diff --git a/extensions/vscode/src/api/package.ts b/packages/language-core/src/api/package.ts similarity index 76% rename from extensions/vscode/src/api/package.ts rename to packages/language-core/src/api/package.ts index d2b5abd..b094022 100644 --- a/extensions/vscode/src/api/package.ts +++ b/packages/language-core/src/api/package.ts @@ -1,9 +1,8 @@ import type { MaybeError, PackageVersionsInfoWithMetadata } from 'fast-npm-meta' -import { logger } from '#state' -import { createBatchRunner } from '#utils/batch' -import { CACHE_MAX_AGE_ONE_DAY } from '#utils/constants' import { getVersionsBatch } from 'fast-npm-meta' import { defineCachedFunction } from 'ocache' +import { CACHE_MAX_AGE_ONE_DAY } from '../constants' +import { createBatchRunner } from '../utils/batch' const BATCH_SIZE = 20 @@ -11,10 +10,8 @@ export interface PackageInfo extends PackageVersionsInfoWithMetadata { versionToTag: Map } -function parsePackageInfo(name: string, pkg: MaybeError) { +function parsePackageInfo(pkg: MaybeError) { if ('error' in pkg) { - logger.warn(`[package] Fetching error(${name}): ${JSON.stringify(pkg)}`) - // Return null to trigger a cache hit if (pkg.status === 404) return null @@ -35,16 +32,11 @@ function parsePackageInfo(name: string, pkg: MaybeError({ maxSize: BATCH_SIZE, runBatch: async (names) => { - const logName = names.join(', ') - logger.info(`[package] Fetching ${logName}`) - const list = await getVersionsBatch(names, { metadata: true, throw: false, }) - logger.info(`[package] Fetched ${logName}`) - const values = new Map() const errors = new Map() @@ -56,7 +48,7 @@ const getPackageInfoBatch = createBatchRunner({ } try { - values.set(name, parsePackageInfo(name, item)) + values.set(name, parsePackageInfo(item)) } catch (error) { errors.set(name, error) } diff --git a/extensions/vscode/src/api/replacement.ts b/packages/language-core/src/api/replacement.ts similarity index 68% rename from extensions/vscode/src/api/replacement.ts rename to packages/language-core/src/api/replacement.ts index aa3a0a3..f0d7732 100644 --- a/extensions/vscode/src/api/replacement.ts +++ b/packages/language-core/src/api/replacement.ts @@ -1,18 +1,15 @@ import type { ModuleReplacement } from 'module-replacements' -import { logger } from '#state' -import { CACHE_MAX_AGE_ONE_DAY, NPMX_DEV_API } from '#utils/constants' -import { encodePackageName } from '#utils/package' import { defineCachedFunction } from 'ocache' import { ofetch } from 'ofetch' +import { CACHE_MAX_AGE_ONE_DAY, NPMX_DEV_API } from '../constants' +import { encodePackageName } from '../utils/package' export const getReplacement = defineCachedFunction(async (name) => { - logger.info(`[replacement] fetching for ${name}`) const encodedName = encodePackageName(name) const result = await ofetch(`${NPMX_DEV_API}/replacements/${encodedName}`, { ignoreResponseError: true, }) ?? null - logger.info(`[replacement] fetched for ${name}`) return result }, { diff --git a/extensions/vscode/src/api/vulnerability.ts b/packages/language-core/src/api/vulnerability.ts similarity index 88% rename from extensions/vscode/src/api/vulnerability.ts rename to packages/language-core/src/api/vulnerability.ts index 5cec245..b328302 100644 --- a/extensions/vscode/src/api/vulnerability.ts +++ b/packages/language-core/src/api/vulnerability.ts @@ -1,9 +1,7 @@ -import { logger } from '#state' -import { CACHE_MAX_AGE_ONE_DAY, NPMX_DEV_API } from '#utils/constants' -import { encodePackageName } from '#utils/package' -import { formatPackageId } from 'npmx-language-core/utils' import { defineCachedFunction } from 'ocache' import { ofetch } from 'ofetch' +import { CACHE_MAX_AGE_ONE_DAY, NPMX_DEV_API } from '../constants' +import { encodePackageName, formatPackageId } from '../utils/package' /** * Severity levels in priority order (highest first) @@ -91,13 +89,11 @@ export interface VulnerabilityTreeResult { } export const getVulnerability = defineCachedFunction(async (name, version) => { - logger.info(`[vulnerability] fetching for ${formatPackageId(name, version)}`) const encodedName = encodePackageName(name) const result = await ofetch(`${NPMX_DEV_API}/registry/vulnerabilities/${encodedName}/v/${version}`, { ignoreResponseError: true, }) ?? null - logger.info(`[vulnerability] fetched for ${name}`) return result }, { diff --git a/packages/language-core/src/constants.ts b/packages/language-core/src/constants.ts index 9292139..0c888d3 100644 --- a/packages/language-core/src/constants.ts +++ b/packages/language-core/src/constants.ts @@ -1,3 +1,8 @@ export const PACKAGE_JSON_BASENAME = 'package.json' export const PNPM_WORKSPACE_BASENAME = 'pnpm-workspace.yaml' export const YARN_WORKSPACE_BASENAME = '.yarnrc.yml' + +export const CACHE_MAX_AGE_ONE_DAY = 60 * 60 * 24 + +export const NPMX_DEV = 'https://npmx.dev' +export const NPMX_DEV_API = `${NPMX_DEV}/api` diff --git a/extensions/vscode/src/utils/links.ts b/packages/language-core/src/links.ts similarity index 95% rename from extensions/vscode/src/utils/links.ts rename to packages/language-core/src/links.ts index ef5bc12..681505a 100644 --- a/extensions/vscode/src/utils/links.ts +++ b/packages/language-core/src/links.ts @@ -1,4 +1,4 @@ -import { NPMX_DEV } from '#utils/constants' +import { NPMX_DEV } from './constants' const SPACES = /\s+/g diff --git a/packages/language-core/src/types.ts b/packages/language-core/src/types.ts index 78f236c..2e3e69d 100644 --- a/packages/language-core/src/types.ts +++ b/packages/language-core/src/types.ts @@ -1,3 +1,5 @@ +import type { Engines } from 'fast-npm-meta' + export type OffsetRange = [start: number, end: number] export type DependencyProtocol @@ -39,7 +41,7 @@ interface HasDependenciesInfo { dependencies: ExtractedDependencyInfo[] } -export type Engines = Record +export { Engines } export interface PackageManifestInfo extends HasDependenciesInfo { name?: string diff --git a/extensions/vscode/src/utils/batch.test.ts b/packages/language-core/src/utils/batch.test.ts similarity index 100% rename from extensions/vscode/src/utils/batch.test.ts rename to packages/language-core/src/utils/batch.test.ts diff --git a/extensions/vscode/src/utils/batch.ts b/packages/language-core/src/utils/batch.ts similarity index 100% rename from extensions/vscode/src/utils/batch.ts rename to packages/language-core/src/utils/batch.ts diff --git a/packages/language-core/src/utils/file.ts b/packages/language-core/src/utils/file.ts new file mode 100644 index 0000000..525d327 --- /dev/null +++ b/packages/language-core/src/utils/file.ts @@ -0,0 +1,21 @@ +import { basename } from 'pathe' +import { PACKAGE_JSON_BASENAME, PNPM_WORKSPACE_BASENAME, YARN_WORKSPACE_BASENAME } from '../constants' + +const SUPPORTED_BASENAMES = new Set([ + PACKAGE_JSON_BASENAME, + PNPM_WORKSPACE_BASENAME, + YARN_WORKSPACE_BASENAME, +]) + +export function isDependencyFile(path: string): boolean { + return SUPPORTED_BASENAMES.has(basename(path)) +} + +export function isPackageManifest(path: string): path is `${string}/${typeof PACKAGE_JSON_BASENAME}` { + return path.endsWith(`/${PACKAGE_JSON_BASENAME}`) +} + +export function isWorkspaceFile(path: string): path is `${string}/${typeof PNPM_WORKSPACE_BASENAME}` | `${string}/${typeof YARN_WORKSPACE_BASENAME}` { + return path.endsWith(`/${PNPM_WORKSPACE_BASENAME}`) + || path.endsWith(`/${YARN_WORKSPACE_BASENAME}`) +} diff --git a/extensions/vscode/src/utils/shared.ts b/packages/language-core/src/utils/general.ts similarity index 100% rename from extensions/vscode/src/utils/shared.ts rename to packages/language-core/src/utils/general.ts diff --git a/extensions/vscode/src/utils/ignore.test.ts b/packages/language-core/src/utils/ignore.test.ts similarity index 100% rename from extensions/vscode/src/utils/ignore.test.ts rename to packages/language-core/src/utils/ignore.test.ts diff --git a/extensions/vscode/src/utils/ignore.ts b/packages/language-core/src/utils/ignore.ts similarity index 84% rename from extensions/vscode/src/utils/ignore.ts rename to packages/language-core/src/utils/ignore.ts index d7aa6a6..7777ebf 100644 --- a/extensions/vscode/src/utils/ignore.ts +++ b/packages/language-core/src/utils/ignore.ts @@ -1,4 +1,4 @@ -import { formatPackageId, parsePackageId } from 'npmx-language-core/utils' +import { formatPackageId, parsePackageId } from './package' export function checkIgnored(options: { ignoreList: string[] diff --git a/packages/language-core/src/utils/index.ts b/packages/language-core/src/utils/index.ts index a809673..50b94e0 100644 --- a/packages/language-core/src/utils/index.ts +++ b/packages/language-core/src/utils/index.ts @@ -1,4 +1,8 @@ +export * from './batch' export * from './catalog' export * from './dependency' +export * from './file' +export * from './general' +export * from './ignore' export * from './package' export * from './source-import' diff --git a/packages/language-core/src/utils/package.test.ts b/packages/language-core/src/utils/package.test.ts index 2fcae8f..8f2c988 100644 --- a/packages/language-core/src/utils/package.test.ts +++ b/packages/language-core/src/utils/package.test.ts @@ -1,5 +1,6 @@ +import type { PackageInfo } from '../api/package' import { describe, expect, it } from 'vitest' -import { isJsrNpmPackage, jsrNpmToJsrName, parsePackageId } from './package' +import { encodePackageName, isJsrNpmPackage, jsrNpmToJsrName, parsePackageId, resolveExactVersion } from './package' describe('parsePackageId', () => { it('should parse package id with version', () => { @@ -42,3 +43,44 @@ describe('jsrNpmToJsrName', () => { expect(jsrNpmToJsrName('@jsr/std__path')).toBe('@std/path') }) }) + +describe('encodePackageName', () => { + it('should encode regular package name', () => { + expect(encodePackageName('lodash')).toBe('lodash') + }) + + it('should encode scoped package name', () => { + expect(encodePackageName('@vue/core')).toBe('@vue%2Fcore') + }) +}) + +describe('resolveExactVersion', () => { + it.each([ + ['', '4.10.0'], + ['*', '4.10.0'], + ['^3.0.0', '3.1.0'], + ['^4.0.0', '4.10.0'], + ['^5.0.0', null], + ['latest', '4.10.0'], + ['next', '4.11.0-beta.1'], + ['beta', null], + ])('should resolve $0 to $1', (spec, version) => { + const pkg = { + distTags: { + latest: '4.10.0', + next: '4.11.0-beta.1', + }, + versionsMeta: { + '3.0.0': {}, + '3.1.0': {}, + '4.10.0': {}, + '4.10.1': { + deprecated: 'Unplanned Release', + }, + '4.11.0-beta.1': {}, + }, + } as unknown as PackageInfo + + expect(resolveExactVersion(pkg, spec)).toBe(version) + }) +}) diff --git a/packages/language-core/src/utils/package.ts b/packages/language-core/src/utils/package.ts index c4f6725..af97487 100644 --- a/packages/language-core/src/utils/package.ts +++ b/packages/language-core/src/utils/package.ts @@ -1,3 +1,9 @@ +import type { PackageInfo } from '../api/package' +import Range from 'semver/classes/range' +import gt from 'semver/functions/gt' +import lte from 'semver/functions/lte' +import satisfies from 'semver/functions/satisfies' + export function formatPackageId(name: string, version: string): string { return `${name}@${version}` } @@ -36,3 +42,63 @@ export function jsrNpmToJsrName(name: string): string { return bare return `@${bare.slice(0, separatorIndex)}/${bare.slice(separatorIndex + 2)}` } + +/** + * Encode a package name for use in npm registry URLs. + * Handles scoped packages (e.g., @scope/name -> @scope%2Fname). + */ +export function encodePackageName(name: string): string { + if (name.startsWith('@')) { + return `@${encodeURIComponent(name.slice(1))}` + } + return encodeURIComponent(name) +} + +/** + * Resolve the maximum version satisfying the given range, capped by the `latest` dist-tag when possible. + * + * This first reads the `latest` tag, then selects the highest version that satisfies the range + * without exceeding that `latest` version. + * + * Inspired by: + * https://github.com/antfu-collective/taze/blob/fed751d777620ddb0a0e77a05ea1412f6332d043/src/utils/versions.ts#L66-L104 + */ +function getMaxSatisfying(versions: string[], current: string, tags: PackageInfo['distTags']) { + let version: string | null = null + + try { + const range = new Range(current) + + let maxVersion: string | null = tags.latest + if (!satisfies(maxVersion, range)) + maxVersion = null + + for (const ver of versions) { + if (!satisfies(ver, range)) + continue + + if (!maxVersion || lte(ver, maxVersion)) { + if (!version || gt(ver, version)) { + version = ver + } + } + } + return version + } catch { + return null + } +} + +export function resolveExactVersion(pkg: PackageInfo, version: string) { + if (version === '' || version === '*') + version = 'latest' + + if (Object.hasOwn(pkg.distTags, version)) + return pkg.distTags[version]! + + const versions = Object.keys(pkg.versionsMeta) + if (versions.length === 0) + return null + + return getMaxSatisfying(versions, version, pkg.distTags) +} diff --git a/packages/language-core/tsdown.config.ts b/packages/language-core/tsdown.config.ts index d9b6a29..7f6ed0f 100644 --- a/packages/language-core/tsdown.config.ts +++ b/packages/language-core/tsdown.config.ts @@ -9,8 +9,10 @@ export default defineConfig({ }, /// keep-sorted entry: [ + 'src/api/*', 'src/constants.ts', 'src/extractors/index.ts', + 'src/links.ts', 'src/types.ts', 'src/utils/index.ts', ], @@ -21,7 +23,10 @@ export default defineConfig({ deps: { /// keep-sorted onlyBundle: [ + 'fast-npm-meta', 'jsonc-parser', + 'module-replacements', + 'ofetch', 'pathe', 'yaml', ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06cf9b2..e280f52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,21 +136,12 @@ importers: '@types/vscode': specifier: 1.101.0 version: 1.101.0 - fast-npm-meta: - specifier: catalog:inline - version: 1.4.2 npmx-language-core: specifier: workspace:* version: link:../../packages/language-core ocache: specifier: catalog:inline version: 0.1.2 - ofetch: - specifier: catalog:inline - version: 2.0.0-alpha.3 - pathe: - specifier: catalog:inline - version: 2.0.3 perfect-debounce: specifier: catalog:inline version: 2.1.0 @@ -168,10 +159,26 @@ importers: version: 0.1.1(@types/vscode@1.101.0) packages/language-core: + dependencies: + ocache: + specifier: catalog:inline + version: 0.1.2 + ofetch: + specifier: catalog:inline + version: 2.0.0-alpha.3 + semver: + specifier: catalog:inline + version: 7.7.4 devDependencies: + fast-npm-meta: + specifier: catalog:inline + version: 1.4.2 jsonc-parser: specifier: catalog:inline version: 3.3.1 + module-replacements: + specifier: catalog:test + version: 2.11.0 pathe: specifier: catalog:inline version: 2.0.3 From 3550942b79c8a3c1cc10a59bc51f2d5ad52e920f Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Sat, 21 Mar 2026 16:45:08 +0800 Subject: [PATCH 2/2] update --- extensions/vscode/src/providers/diagnostics/index.ts | 2 ++ extensions/vscode/src/providers/hover/resolve.ts | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/extensions/vscode/src/providers/diagnostics/index.ts b/extensions/vscode/src/providers/diagnostics/index.ts index 3e991b4..71bf008 100644 --- a/extensions/vscode/src/providers/diagnostics/index.ts +++ b/extensions/vscode/src/providers/diagnostics/index.ts @@ -127,6 +127,8 @@ export function useDiagnostics() { return const document = activeEditor.value.document + if (document.uri.scheme !== 'file') + return if (!isDependencyFile(document.uri.path)) return diff --git a/extensions/vscode/src/providers/hover/resolve.ts b/extensions/vscode/src/providers/hover/resolve.ts index 45edfcd..b56f526 100644 --- a/extensions/vscode/src/providers/hover/resolve.ts +++ b/extensions/vscode/src/providers/hover/resolve.ts @@ -9,14 +9,14 @@ export async function resolveHoverDependency( document: TextDocument, position: Position, ): Promise { + if (document.uri.scheme !== 'file') + return + const offset = document.offsetAt(position) if (isDependencyFile(document.uri.path)) return await getResolvedDependencyByOffset(document.uri, offset) - if (document.uri.scheme !== 'file') - return - const wordRange = document.getWordRangeAtPosition(position) if (!wordRange) return