Skip to content

Commit bf1dbff

Browse files
committed
feat: resolve catalog version
1 parent 1ad0b17 commit bf1dbff

File tree

8 files changed

+151
-65
lines changed

8 files changed

+151
-65
lines changed

src/extractors/pnpm-workspace-yaml.ts

Lines changed: 15 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
import type { DependencyInfo, Extractor } from '#types/extractor'
22
import type { TextDocument } from 'vscode'
3-
import type { Node, Pair, Scalar, YAMLMap } from 'yaml'
3+
import type { Node } from 'yaml'
44
import { isInRange } from '#utils/ast'
5+
import { traverseYamlCatalogs } from '#utils/catalog/yaml'
56
import { parseYaml } from '#utils/parse'
67
import { Range } from 'vscode'
7-
import { isMap, isPair, isScalar } from 'yaml'
8-
9-
const CATALOG_SECTION = 'catalog'
10-
const CATALOGS_SECTION = 'catalogs'
11-
12-
type CatalogEntry = Pair<Scalar<string>, Scalar<string>>
13-
14-
type CatalogEntryVisitor = (catalog: CatalogEntry) => boolean | void
8+
import { isMap } from 'yaml'
159

1610
export class PnpmWorkspaceYamlExtractor implements Extractor<Node> {
1711
parse = parseYaml
@@ -31,66 +25,34 @@ export class PnpmWorkspaceYamlExtractor implements Extractor<Node> {
3125

3226
const result: DependencyInfo<Node>[] = []
3327

34-
this.traverseCatalogs(root, (item) => {
28+
traverseYamlCatalogs(root, (entry) => {
3529
result.push({
36-
nameNode: item.key,
37-
versionNode: item.value!,
38-
name: String(item.key.value),
39-
version: String(item.value!.value),
30+
nameNode: entry.key,
31+
versionNode: entry.value!,
32+
name: String(entry.key.value),
33+
version: String(entry.value!.value),
4034
})
4135
})
4236

4337
return result
4438
}
4539

46-
private traverseCatalogs(root: YAMLMap, callback: CatalogEntryVisitor): boolean {
47-
const catalog = root.items.find((i) => isScalar(i.key) && i.key.value === CATALOG_SECTION)
48-
if (this.traverseCatalog(catalog, callback))
49-
return true
50-
51-
const catalogs = root.items.find((i) => isScalar(i.key) && i.key.value === CATALOGS_SECTION)
52-
if (isMap(catalogs?.value)) {
53-
for (const c of catalogs.value.items) {
54-
if (this.traverseCatalog(c, callback))
55-
return true
56-
}
57-
}
58-
59-
return false
60-
}
61-
62-
private traverseCatalog(catalog: unknown, callback: CatalogEntryVisitor): boolean {
63-
if (!isPair(catalog))
64-
return false
65-
if (!isMap(catalog.value))
66-
return false
67-
68-
for (const item of catalog.value.items) {
69-
if (isScalar(item.key) && isScalar(item.value)) {
70-
if (callback(item as CatalogEntry))
71-
return true
72-
}
73-
}
74-
75-
return false
76-
}
77-
7840
getDependencyInfoByOffset(root: Node, offset: number): DependencyInfo<Node> | undefined {
7941
if (!isMap(root))
8042
return
8143

8244
let result: DependencyInfo<Node> | undefined
8345

84-
this.traverseCatalogs(root, (item) => {
46+
traverseYamlCatalogs(root, (entry) => {
8547
if (
86-
isInRange(offset, item.value!.range!)
87-
|| isInRange(offset, item.key.range!)
48+
isInRange(offset, entry.value!.range!)
49+
|| isInRange(offset, entry.key.range!)
8850
) {
8951
result = {
90-
nameNode: item.key,
91-
versionNode: item.value!,
92-
name: String(item.key.value),
93-
version: String(item.value!.value),
52+
nameNode: entry.key,
53+
versionNode: entry.value!,
54+
name: String(entry.key.value),
55+
version: String(entry.value!.value),
9456
}
9557
return true
9658
}

src/providers/completion-item/version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class VersionCompletionItemProvider<T extends Extractor> implements Compl
3030
} = info
3131

3232
const parsed = parseVersion(version)
33-
if (!parsed || !isSupportedProtocol(parsed.protocol))
33+
if (!parsed || !isSupportedProtocol(parsed.protocol) || parsed.protocol === 'catalog')
3434
return
3535

3636
const pkg = await getPackageInfo(name)

src/providers/diagnostics/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { Diagnostic, TextDocument } from 'vscode'
77
import { useActiveExtractor } from '#composables/active-extractor'
88
import { config, logger } from '#state'
99
import { getPackageInfo } from '#utils/api/package'
10+
import { resolveCatalogVersion } from '#utils/catalog'
1011
import { resolveExactVersion } from '#utils/package'
1112
import { isSupportedProtocol, parseVersion } from '#utils/version'
1213
import { debounce } from 'perfect-debounce'
@@ -102,7 +103,15 @@ export function useDiagnostics() {
102103
if (!pkg)
103104
continue
104105

105-
const parsed = parseVersion(dep.version)
106+
let parsed = parseVersion(dep.version)
107+
108+
if (parsed?.protocol === 'catalog') {
109+
const resolved = await resolveCatalogVersion(document.uri, dep.name, parsed.version)
110+
if (isDocumentChanged(document, targetUri, targetVersion))
111+
return
112+
parsed = resolved ? { protocol: 'catalog', version: resolved } : null
113+
}
114+
106115
const exactVersion = parsed && isSupportedProtocol(parsed.protocol)
107116
? resolveExactVersion(pkg, parsed.version)
108117
: null

src/providers/hover/npmx.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Extractor } from '#types/extractor'
22
import type { HoverProvider, Position, TextDocument } from 'vscode'
33
import { SPACER } from '#constants'
44
import { getPackageInfo } from '#utils/api/package'
5+
import { resolveCatalogVersion } from '#utils/catalog'
56
import { jsrPackageUrl, npmxDocsUrl, npmxPackageUrl } from '#utils/links'
67
import { resolveExactVersion } from '#utils/package'
78
import { isSupportedProtocol, parseVersion } from '#utils/version'
@@ -29,7 +30,14 @@ export class NpmxHoverProvider<T extends Extractor> implements HoverProvider {
2930
return
3031

3132
const { name } = dep
32-
const { protocol, version } = parsed
33+
let { protocol, version } = parsed
34+
35+
if (protocol === 'catalog') {
36+
const resolved = await resolveCatalogVersion(document.uri, name, version)
37+
if (!resolved)
38+
return
39+
version = resolved
40+
}
3341

3442
if (protocol === 'jsr') {
3543
const jsrMd = new MarkdownString('', true)

src/utils/catalog.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { PNPM_WORKSPACE_BASENAME, YARN_WORKSPACE_BASENAME } from '#constants'
2+
import { memoize } from '#utils/memoize'
3+
import { parseYaml } from '#utils/parse'
4+
import { findNearestFile } from '#utils/resolve'
5+
import { Uri, workspace } from 'vscode'
6+
import { extractYamlCatalogs } from './catalog/yaml'
7+
8+
export type WorkspaceCatalogs = Map<string, Map<string, string>>
9+
10+
function createWorkspaceFolderStop(): (uri: Uri) => boolean {
11+
const roots = new Set(workspace.workspaceFolders?.map((f) => f.uri.toString()) ?? [])
12+
return (uri) => roots.has(uri.toString())
13+
}
14+
15+
const findWorkspaceUri = memoize(
16+
async (fileUri: Uri) =>
17+
await findNearestFile([PNPM_WORKSPACE_BASENAME, YARN_WORKSPACE_BASENAME], Uri.joinPath(fileUri, '..'), createWorkspaceFolderStop()) ?? null,
18+
{ getKey: (uri) => Uri.joinPath(uri, '..').toString() },
19+
)
20+
21+
async function getCatalogs(fileUri: Uri) {
22+
const workspaceUri = await findWorkspaceUri(fileUri)
23+
if (!workspaceUri)
24+
return
25+
26+
const doc = await workspace.openTextDocument(workspaceUri)
27+
const root = parseYaml(doc)
28+
if (!root)
29+
return
30+
31+
return extractYamlCatalogs(root)
32+
}
33+
34+
export async function resolveCatalogVersion(fileUri: Uri, packageName: string, catalogName: string): Promise<string | undefined> {
35+
const catalogs = await getCatalogs(fileUri)
36+
if (!catalogs)
37+
return
38+
39+
return catalogs.get(catalogName || 'default')?.get(packageName)
40+
}

src/utils/catalog/yaml.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { WorkspaceCatalogs } from '#utils/catalog'
2+
import type { Node, Pair, Scalar, YAMLMap } from 'yaml'
3+
import { isMap, isPair, isScalar } from 'yaml'
4+
5+
export type YamlCatalogEntry = Pair<Scalar<string>, Scalar<string>>
6+
7+
export function traverseYamlCatalogs(root: YAMLMap, callback: (entry: YamlCatalogEntry, catalogName: string) => boolean | void): boolean {
8+
const catalog = root.items.find((i) => isScalar(i.key) && i.key.value === 'catalog')
9+
if (visitCatalogEntries(catalog, (entry) => callback(entry, '')))
10+
return true
11+
12+
const catalogs = root.items.find((i) => isScalar(i.key) && i.key.value === 'catalogs')
13+
if (isMap(catalogs?.value)) {
14+
for (const c of catalogs.value.items) {
15+
if (!isScalar(c.key))
16+
continue
17+
const name = String(c.key.value)
18+
if (visitCatalogEntries(c, (entry) => callback(entry, name)))
19+
return true
20+
}
21+
}
22+
23+
return false
24+
}
25+
26+
function visitCatalogEntries(catalog: unknown, callback: (entry: YamlCatalogEntry) => boolean | void): boolean {
27+
if (!isPair(catalog) || !isMap(catalog.value))
28+
return false
29+
30+
for (const item of catalog.value.items) {
31+
if (isScalar(item.key) && isScalar(item.value)) {
32+
if (callback(item as YamlCatalogEntry))
33+
return true
34+
}
35+
}
36+
37+
return false
38+
}
39+
40+
export function extractYamlCatalogs(root: Node): WorkspaceCatalogs {
41+
const result: WorkspaceCatalogs = new Map()
42+
43+
if (!isMap(root))
44+
return result
45+
46+
traverseYamlCatalogs(root, (entry, catalogName) => {
47+
if (!entry.value?.range)
48+
return
49+
50+
const name = String(entry.key.value)
51+
const version = String(entry.value.value)
52+
53+
catalogName ||= 'default'
54+
let catalog = result.get(catalogName)
55+
if (!catalog) {
56+
catalog = new Map()
57+
result.set(catalogName, catalog)
58+
}
59+
60+
catalog.set(name, version)
61+
})
62+
63+
return result
64+
}

src/utils/resolve.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@ export function* walkAncestors(start: Uri, shouldStop?: (uri: Uri) => boolean):
1616
}
1717
}
1818

19-
export async function findNearestFile(filename: string, start: Uri, shouldStop?: (uri: Uri) => boolean): Promise<Uri | undefined> {
19+
export async function findNearestFile(filenames: string | string[], start: Uri, shouldStop?: (uri: Uri) => boolean): Promise<Uri | undefined> {
20+
const names = Array.isArray(filenames) ? filenames : [filenames]
2021
for (const dir of walkAncestors(start, shouldStop)) {
21-
const fileUri = Uri.joinPath(dir, filename)
22-
try {
23-
await workspace.fs.stat(fileUri)
24-
return fileUri
25-
} catch {
26-
continue
22+
for (const filename of names) {
23+
const fileUri = Uri.joinPath(dir, filename)
24+
try {
25+
await workspace.fs.stat(fileUri)
26+
return fileUri
27+
} catch {
28+
continue
29+
}
2730
}
2831
}
2932
}

src/utils/version.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ function isUrlPackage(currentVersion: string) {
55
return URL_PACKAGE_PATTERN.test(currentVersion)
66
}
77

8-
const UNSUPPORTED_PROTOCOLS = new Set(['workspace', 'catalog', 'jsr'])
9-
const KNOWN_PROTOCOLS = new Set([...UNSUPPORTED_PROTOCOLS, 'npm'])
8+
const UNSUPPORTED_PROTOCOLS = new Set(['workspace', 'jsr'])
9+
const KNOWN_PROTOCOLS = new Set([...UNSUPPORTED_PROTOCOLS, 'catalog', 'npm'])
1010

1111
export interface ParsedVersion {
1212
protocol: VersionProtocol

0 commit comments

Comments
 (0)