Skip to content
Merged
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: 2 additions & 3 deletions extensions/vscode/src/composables/workspace-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@ export function useWorkspaceContext() {
if (!ctx)
return

await ctx.invalidateDependencyInfo(uri)
await ctx.invalidateDependencyInfo(uri.path)
logger.info(`[workspace-context] invalidate dependencies cache: ${uri.path}`)
if (reload) {
const folderPath = ctx!.folder.uri.path
const isRoot = uri.path === `${folderPath}/${PACKAGE_JSON_BASENAME}`
const isRoot = uri.path === `${ctx.rootPath}/${PACKAGE_JSON_BASENAME}`
if (isRoot || isWorkspaceFile(uri.path))
await ctx.loadWorkspace()
}
Expand Down
206 changes: 30 additions & 176 deletions extensions/vscode/src/core/workspace.ts
Original file line number Diff line number Diff line change
@@ -1,182 +1,36 @@
import type { PackageManager } from '#shared/types'
import type { PackageInfo } from 'npmx-language-core/api/package'
import type {
CatalogsInfo,
ExtractedDependencyInfo,
PackageManifestInfo,
ResolvedDependencyInfo,
WorkspaceCatalogInfo,
} from 'npmx-language-core/types'
import type { CacheOptions } from 'ocache'
import type { WorkspaceFolder } from 'vscode'
import type { DependencyInfo, WorkspaceAdapter } from 'npmx-language-core/workspace'
import type { Uri } from 'vscode'
import { logger } from '#state'
import { isOffsetInRange } from '#utils/ast'
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 { isPackageManifest, isWorkspaceFile, lazyInit, resolveDependencySpec, resolveExactVersion } from 'npmx-language-core/utils'
import { isPackageManifest } from 'npmx-language-core/utils'
import { WorkspaceContext } from 'npmx-language-core/workspace'
import { defineCachedFunction } from 'ocache'
import { commands, Uri, window, workspace } from 'vscode'
import { commands, window, workspace } from 'vscode'
import { accessOk } from 'vscode-find-up'

export interface DependencyInfo extends ExtractedDependencyInfo, ResolvedDependencyInfo {
packageInfo: () => Promise<PackageInfo | null>
resolvedVersion: () => Promise<string | null>
}

type WithDependencyInfo<T> = Omit<T, 'dependencies'> & {
dependencies: DependencyInfo[]
}

export const workspaceFileMapping: Record<Exclude<PackageManager, 'npm'>, string> = {
pnpm: PNPM_WORKSPACE_BASENAME,
yarn: YARN_WORKSPACE_BASENAME,
}

async function getPackageManager(uri: Uri): Promise<PackageManager> {
try {
const result = await commands.executeCommand<PackageManager>('npm.packageManager', uri)
return result || 'npm'
} catch (error) {
logger.error('Error getting package manager:', error)
window.showErrorMessage('Failed to detect package manager. Defaulting to npm.')
return 'npm'
}
}

class WorkspaceContext {
folder: WorkspaceFolder
packageManager: PackageManager = 'npm'
workspaceFileUri?: Uri
#catalogs?: PromiseWithResolvers<CatalogsInfo | undefined>

private constructor(folder: WorkspaceFolder) {
this.folder = folder
}

static async create(folder: WorkspaceFolder): Promise<WorkspaceContext> {
const ctx = new WorkspaceContext(folder)
await ctx.loadWorkspace()

return ctx
}

async loadWorkspace() {
this.#catalogs = Promise.withResolvers()
this.packageManager = await getPackageManager(this.folder.uri)

logger.info(`[workspace-context] detect package manager: ${this.packageManager}`)

if (this.packageManager !== 'npm') {
const workspaceFilename = workspaceFileMapping[this.packageManager]
this.workspaceFileUri = Uri.joinPath(this.folder.uri, workspaceFilename)
this.#catalogs.resolve(
await accessOk(this.workspaceFileUri)
? (await this.loadWorkspaceFileInfo(this.workspaceFileUri))?.catalogs
: undefined,
)
} else {
this.#catalogs.resolve(undefined)
}
}

#cacheOptions: CacheOptions<any, [Uri]> = {
getKey: (uri) => uri.path,
maxAge: 0,
swr: false,
staleMaxAge: 0,
}

async getCatalogs(): Promise<CatalogsInfo | undefined> {
return this.#catalogs!.promise
}

#createResolvedDependencyInfo(dependency: ExtractedDependencyInfo, catalogs?: CatalogsInfo): DependencyInfo {
const resolution = resolveDependencySpec(dependency.rawName, dependency.rawSpec, catalogs)

const packageInfo = lazyInit(
async () => resolution.resolvedProtocol === 'npm'
? await getPackageInfo(resolution.resolvedName) ?? null
: null,
)

return {
...dependency,
...resolution,
categoryName: dependency.categoryName ?? resolution.categoryName,
packageInfo,
resolvedVersion: lazyInit(async () => {
if (resolution.resolvedProtocol !== 'npm')
return null

const pkg = await packageInfo()
if (!pkg)
return null

return resolveExactVersion(pkg, resolution.resolvedSpec)
}),
}
}

loadPackageManifestInfo = defineCachedFunction<
WithDependencyInfo<PackageManifestInfo> | undefined,
[Uri]
>(async (uri) => {
const path = uri.path
if (!isPackageManifest(path))
return

logger.info(`[workspace-context] load package manifest info: ${path}`)

const extractor = getExtractor(path)
if (!extractor)
return

const [info, catalogs] = await Promise.all([
getDocumentText(uri).then((text) => extractor.getPackageManifestInfo(text)),
this.getCatalogs(),
])

if (!info)
return

return {
...info,
dependencies: info.dependencies.map((dep) => this.#createResolvedDependencyInfo(dep, catalogs)),
}
}, this.#cacheOptions)

loadWorkspaceFileInfo = defineCachedFunction<
WithDependencyInfo<WorkspaceCatalogInfo> | undefined,
[Uri]
>(async (uri) => {
const path = uri.path
if (!isWorkspaceFile(path))
return
logger.info(`[workspace-context] load workspace catalog info: ${path}`)

const extractor = getExtractor(path)
if (!extractor)
return

const text = await getDocumentText(uri)
const info = extractor.getWorkspaceCatalogInfo(text)

if (!info)
return

return {
...info,
dependencies: info.dependencies.map((dep) => this.#createResolvedDependencyInfo(dep)),
}
}, this.#cacheOptions)

async invalidateDependencyInfo(uri: Uri) {
if (isPackageManifest(uri.path))
await this.loadPackageManifestInfo.invalidate(uri)
else if (isWorkspaceFile(uri.path))
await this.loadWorkspaceFileInfo.invalidate(uri)
function createVscodeAdapter(baseUri: Uri): WorkspaceAdapter {
const toUri = (path: string) => baseUri.with({ path })

return {
async readFile(path: string): Promise<string> {
return getDocumentText(toUri(path))
},

async fileExists(path: string): Promise<boolean> {
return accessOk(toUri(path))
},

async detectPackageManager(rootPath: string): Promise<'npm' | 'pnpm' | 'yarn'> {
try {
const result = await commands.executeCommand<'npm' | 'pnpm' | 'yarn'>('npm.packageManager', toUri(rootPath))
return result || 'npm'
} catch (error) {
logger.error('Error getting package manager:', error)
window.showErrorMessage('Failed to detect package manager. Defaulting to npm.')
return 'npm'
}
},
}
}

Expand All @@ -189,7 +43,7 @@ export const getWorkspaceContext = defineCachedFunction<
return

logger.info(`[workspace-context] built ${folder.uri.path}`)
return await WorkspaceContext.create(folder)
return await WorkspaceContext.create(folder.uri.path, createVscodeAdapter(folder.uri))
}, {
name: 'workspace-context',
getKey: (uri) => workspace.getWorkspaceFolder(uri)?.uri.path ?? '',
Expand All @@ -205,8 +59,8 @@ export async function getResolvedDependencies(uri: Uri): Promise<DependencyInfo[

return (
isPackageManifest(uri.path)
? await ctx.loadPackageManifestInfo(uri)
: await ctx.loadWorkspaceFileInfo(uri)
? await ctx.loadPackageManifestInfo(uri.path)
: await ctx.loadWorkspaceFileInfo(uri.path)
)?.dependencies
}

Expand Down
9 changes: 5 additions & 4 deletions extensions/vscode/src/providers/definition/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ export class CatalogDefinitionProvider implements DefinitionProvider {
return

const ctx = await getWorkspaceContext(document.uri)
if (!ctx?.workspaceFileUri)
if (!ctx?.workspaceFilePath)
return

const dependencies = (await ctx.loadWorkspaceFileInfo(ctx.workspaceFileUri))?.dependencies
const dependencies = (await ctx.loadWorkspaceFileInfo(ctx.workspaceFilePath))?.dependencies
if (!dependencies)
return

Expand All @@ -28,10 +28,11 @@ export class CatalogDefinitionProvider implements DefinitionProvider {
if (!target)
return

const workspaceDocument = await workspace.openTextDocument(ctx.workspaceFileUri)
const workspaceFileUri = document.uri.with({ path: ctx.workspaceFilePath })
const workspaceDocument = await workspace.openTextDocument(workspaceFileUri)

return new Location(
ctx.workspaceFileUri,
workspaceFileUri,
offsetRangeToRange(workspaceDocument, target.specRange),
)
}
Expand Down
2 changes: 1 addition & 1 deletion extensions/vscode/src/providers/diagnostics/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DependencyInfo } from '#core/workspace'
import type { OffsetRange } from 'npmx-language-core/types'
import type { DependencyInfo } from 'npmx-language-core/workspace'
import type { Awaitable } from 'reactive-vscode'
import type { Diagnostic, TextDocument, Uri } from 'vscode'
import { getResolvedDependencies } from '#core/workspace'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const checkEngineMismatch: DiagnosticRule = async ({ uri, dep, pkg }) =>
return

const ctx = await getWorkspaceContext(uri)
const engines = (await ctx?.loadPackageManifestInfo(uri))?.engines
const engines = (await ctx?.loadPackageManifestInfo(uri.path))?.engines

if (!engines)
return
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DependencyInfo } from '#core/workspace'
import type { PackageInfo } from 'npmx-language-core/api/package'
import type { DependencyInfo } from 'npmx-language-core/workspace'
import { describe, expect, it } from 'vitest'
import { createContext } from './__tests__/utils'
import { resolveUpgrade } from './upgrade'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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 { DependencyInfo } from 'npmx-language-core/workspace'
import type { DiagnosticRule, RangeDiagnosticInfo } from '..'
import { config } from '#state'
import { formatUpgradeVersion } from '#utils/version'
Expand Down
2 changes: 1 addition & 1 deletion extensions/vscode/src/providers/hover/resolve.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DependencyInfo } from '#core/workspace'
import type { DependencyInfo } from 'npmx-language-core/workspace'
import type { Position, TextDocument } from 'vscode'
import { getResolvedDependencies, getResolvedDependencyByOffset } from '#core/workspace'
import { beforeEach, describe, expect, it, vi } from 'vitest'
Expand Down
2 changes: 1 addition & 1 deletion extensions/vscode/src/providers/hover/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DependencyInfo } from '#core/workspace'
import type { DependencyInfo } from 'npmx-language-core/workspace'
import type { Position, TextDocument } from 'vscode'
import { getResolvedDependencies, getResolvedDependencyByOffset } from '#core/workspace'
import { PACKAGE_JSON_BASENAME } from 'npmx-language-core/constants'
Expand Down
2 changes: 1 addition & 1 deletion extensions/vscode/src/utils/version.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DependencyInfo } from '#core/workspace'
import type { DependencyInfo } from 'npmx-language-core/workspace'
import { describe, expect, it } from 'vitest'
import { formatUpgradeVersion } from './version'

Expand Down
2 changes: 1 addition & 1 deletion extensions/vscode/src/utils/version.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DependencyInfo } from '#core/workspace'
import type { DependencyInfo } from 'npmx-language-core/workspace'
import { formatPackageId } from 'npmx-language-core/utils'

const RANGE_PREFIXES = ['>=', '<=', '=', '>', '<']
Expand Down
3 changes: 2 additions & 1 deletion packages/language-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"./links": "./dist/links.js",
"./types": "./dist/types.js",
"./utils": "./dist/utils/index.js",
"./workspace": "./dist/workspace.js",
"./package.json": "./package.json"
},
"files": [
Expand All @@ -41,10 +42,10 @@
"yaml": "catalog:inline"
},
"inlinedDependencies": {
"fast-npm-meta": "1.4.2",
"module-replacements": "2.11.0",
"jsonc-parser": "3.3.1",
"yaml": "2.8.2",
"fast-npm-meta": "1.4.2",
"pathe": "2.0.3"
}
}
Loading
Loading