Skip to content

Commit 8cce1e8

Browse files
RYGRIT9romise
andauthored
feat: support bun package manager and bun workspaces (#106)
* feat: support bun package manager * chore: remove comment * fix: apply coderrabbit suggest * refactor: inline merge helper * fix: * fix: use path-browserify for uri path joins * replace pathe with path-browserify * simplify isWorkspaceFile * simplify code * fix * simplify * update --------- Co-authored-by: Vida Xie <vida_2020@163.com>
1 parent 62f97ed commit 8cce1e8

19 files changed

Lines changed: 464 additions & 43 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
- **Hover Information** &ndash; Quick links to package details and documentation on [npmx.dev](https://npmx.dev), with provenance verification status.
2323
- **Version Completion** &ndash; Autocomplete package versions with provenance filtering and prerelease exclusion support.
24-
- **Workspace-Aware Resolution** &ndash; Dependencies in `package.json`, `pnpm-workspace.yaml`, and `.yarnrc.yml` are resolved from a shared workspace context, including catalogs and workspace references.
24+
- **Workspace-Aware Resolution** &ndash; Dependencies in `package.json`, `pnpm-workspace.yaml`, and `.yarnrc.yml` are resolved from a shared workspace context, including npm, pnpm, yarn, and bun package managers plus root `package.json` catalogs and workspace references.
2525
- **Diagnostics**
2626
- Deprecated package warnings with deprecation messages
2727
- Package replacement suggestions (via [module-replacements](https://github.com/es-tooling/module-replacements))

extensions/vscode/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
- **Hover Information** &ndash; Quick links to package details and documentation on [npmx.dev](https://npmx.dev), with provenance verification status.
2929
- **Version Completion** &ndash; Autocomplete package versions with provenance filtering and prerelease exclusion support.
30-
- **Workspace-Aware Resolution** &ndash; Dependencies in `package.json`, `pnpm-workspace.yaml`, and `.yarnrc.yml` are resolved from a shared workspace context, including catalogs and workspace references.
30+
- **Workspace-Aware Resolution** &ndash; Dependencies in `package.json`, `pnpm-workspace.yaml`, and `.yarnrc.yml` are resolved from a shared workspace context, including npm, pnpm, yarn, and bun package managers plus root `package.json` catalogs and workspace references.
3131
- **Diagnostics**
3232
- Deprecated package warnings with deprecation messages
3333
- Package replacement suggestions (via [module-replacements](https://github.com/es-tooling/module-replacements))

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
},
3434
"devDependencies": {
3535
"@types/node": "catalog:dev",
36+
"@types/path-browserify": "catalog:dev",
3637
"@types/semver": "catalog:dev",
3738
"@typescript/native-preview": "catalog:dev",
3839
"@vida0905/eslint-config": "catalog:dev",

packages/language-core/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@
3737
"fast-npm-meta": "catalog:inline",
3838
"jsonc-parser": "catalog:inline",
3939
"module-replacements": "catalog:test",
40-
"pathe": "catalog:inline",
40+
"path-browserify": "catalog:inline",
4141
"yaml": "catalog:inline"
4242
},
4343
"inlinedDependencies": {
4444
"fast-npm-meta": "1.5.0",
4545
"jsonc-parser": "3.3.1",
4646
"module-replacements": "2.11.0",
47-
"pathe": "2.0.3",
47+
"path-browserify": "1.0.1",
4848
"yaml": "2.8.3"
4949
}
5050
}

packages/language-core/src/extractors/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { extname } from 'pathe'
1+
import { extname } from 'path-browserify'
22
import { JsonExtractor } from './json'
33
import { YamlExtractor } from './yaml'
44

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { JsonExtractor } from './json'
3+
4+
describe('jsonExtractor', () => {
5+
const extractor = new JsonExtractor()
6+
7+
it('extracts bun workspace catalogs from package.json', () => {
8+
const info = extractor.getWorkspaceCatalogInfo(`{
9+
"workspaces": ["packages/*"],
10+
"catalog": {
11+
"lodash": "^4.17.21"
12+
},
13+
"catalogs": {
14+
"prod": {
15+
"@deno/doc": "jsr:^0.189.1"
16+
}
17+
}
18+
}`)
19+
20+
expect(info?.catalogs).toEqual({
21+
default: {
22+
lodash: '^4.17.21',
23+
},
24+
prod: {
25+
'@deno/doc': 'jsr:^0.189.1',
26+
},
27+
})
28+
expect(info?.dependencies.map(({ rawName, rawSpec, categoryName }) => ({
29+
rawName,
30+
rawSpec,
31+
categoryName,
32+
}))).toEqual([
33+
{
34+
rawName: 'lodash',
35+
rawSpec: '^4.17.21',
36+
categoryName: '',
37+
},
38+
{
39+
rawName: '@deno/doc',
40+
rawSpec: 'jsr:^0.189.1',
41+
categoryName: 'prod',
42+
},
43+
])
44+
})
45+
46+
it('extracts catalogs nested inside the workspaces object', () => {
47+
const info = extractor.getWorkspaceCatalogInfo(`{
48+
"workspaces": {
49+
"packages": ["packages/*"],
50+
"catalog": {
51+
"react": "^19.0.0"
52+
},
53+
"catalogs": {
54+
"test": {
55+
"vitest": "^4.0.0"
56+
}
57+
}
58+
}
59+
}`)
60+
61+
expect(info?.catalogs).toEqual({
62+
default: {
63+
react: '^19.0.0',
64+
},
65+
test: {
66+
vitest: '^4.0.0',
67+
},
68+
})
69+
})
70+
})

packages/language-core/src/extractors/json.ts

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import type { Node as JsonNode } from 'jsonc-parser'
2-
import type { BaseExtractor, DependencyCategory, Engines, ExtractedDependencyInfo, OffsetRange, PackageManifestExtractor, PackageManifestInfo } from '../types'
2+
import type {
3+
BaseExtractor,
4+
DependencyCategory,
5+
Engines,
6+
ExtractedDependencyInfo,
7+
OffsetRange,
8+
PackageManifestExtractor,
9+
PackageManifestInfo,
10+
WorkspaceCatalogExtractor,
11+
WorkspaceCatalogInfo,
12+
} from '../types'
313
import { findNodeAtLocation, parseTree } from 'jsonc-parser'
14+
import { normalizeCatalogName } from '../utils'
415

516
const DEPENDENCY_SECTIONS: DependencyCategory[] = [
617
'dependencies',
@@ -9,7 +20,22 @@ const DEPENDENCY_SECTIONS: DependencyCategory[] = [
920
'optionalDependencies',
1021
]
1122

12-
export class JsonExtractor implements PackageManifestExtractor, BaseExtractor<JsonNode> {
23+
interface CatalogMeta {
24+
category: 'catalog' | 'catalogs'
25+
categoryName?: string
26+
}
27+
28+
const CATALOG_NODE_PATHS: {
29+
path: string[]
30+
meta: CatalogMeta
31+
}[] = [
32+
{ path: ['catalog'], meta: { category: 'catalog', categoryName: '' } },
33+
{ path: ['catalogs'], meta: { category: 'catalogs' } },
34+
{ path: ['workspaces', 'catalog'], meta: { category: 'catalog', categoryName: '' } },
35+
{ path: ['workspaces', 'catalogs'], meta: { category: 'catalogs' } },
36+
]
37+
38+
export class JsonExtractor implements PackageManifestExtractor, WorkspaceCatalogExtractor, BaseExtractor<JsonNode> {
1339
parse = (text: string) => parseTree(text) ?? null
1440

1541
#getStringValue(root: JsonNode, key: string): string | undefined {
@@ -43,6 +69,40 @@ export class JsonExtractor implements PackageManifestExtractor, BaseExtractor<Js
4369
}
4470
}
4571

72+
#parseCatalogEntries(node: JsonNode, meta: CatalogMeta): ExtractedDependencyInfo[] {
73+
if (node.type !== 'object' || !node.children)
74+
return []
75+
76+
if (meta.category === 'catalog') {
77+
return node.children
78+
.map((entry) => this.#parseDependencyNode(entry, meta.category))
79+
.flatMap((dependency) => dependency
80+
? [{ ...dependency, categoryName: meta.categoryName }]
81+
: [])
82+
}
83+
84+
const result: ExtractedDependencyInfo[] = []
85+
86+
for (const catalogNode of node.children) {
87+
const [nameNode, valueNode] = catalogNode.children ?? []
88+
if (typeof nameNode?.value !== 'string' || valueNode?.type !== 'object' || !valueNode.children)
89+
continue
90+
91+
for (const entry of valueNode.children) {
92+
const dependency = this.#parseDependencyNode(entry, meta.category)
93+
if (!dependency)
94+
continue
95+
96+
result.push({
97+
...dependency,
98+
categoryName: nameNode.value,
99+
})
100+
}
101+
}
102+
103+
return result
104+
}
105+
46106
#getEngines(root: JsonNode): Engines | undefined {
47107
const enginesNode = findNodeAtLocation(root, ['engines'])
48108
if (enginesNode?.type !== 'object' || !enginesNode.children?.length)
@@ -81,6 +141,30 @@ export class JsonExtractor implements PackageManifestExtractor, BaseExtractor<Js
81141
return result
82142
}
83143

144+
getWorkspaceCatalogInfo(text: string): WorkspaceCatalogInfo | undefined {
145+
const root = this.parse(text)
146+
if (!root)
147+
return
148+
149+
const dependencies = CATALOG_NODE_PATHS.flatMap(({ path, meta }) => {
150+
const node = findNodeAtLocation(root, path)
151+
return node ? this.#parseCatalogEntries(node, meta) : []
152+
})
153+
154+
const catalogs: Record<string, Record<string, string>> = {}
155+
156+
for (const dependency of dependencies) {
157+
const categoryName = normalizeCatalogName(dependency.categoryName ?? '')
158+
catalogs[categoryName] ??= {}
159+
catalogs[categoryName][dependency.rawName] = dependency.rawSpec
160+
}
161+
162+
return {
163+
dependencies,
164+
catalogs: Object.keys(catalogs).length > 0 ? catalogs : undefined,
165+
}
166+
}
167+
84168
getPackageManifestInfo(text: string): PackageManifestInfo | undefined {
85169
const root = this.parse(text)
86170
if (!root)

packages/language-core/src/utils/file.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { basename } from 'pathe'
1+
import { basename } from 'path-browserify'
22
import { PACKAGE_JSON_BASENAME, PNPM_WORKSPACE_BASENAME, YARN_WORKSPACE_BASENAME } from '../constants'
33

44
const SUPPORTED_BASENAMES = new Set([
@@ -14,8 +14,3 @@ export function isDependencyFile(path: string): boolean {
1414
export function isPackageManifest(path: string): path is `${string}/${typeof PACKAGE_JSON_BASENAME}` {
1515
return path.endsWith(`/${PACKAGE_JSON_BASENAME}`)
1616
}
17-
18-
export function isWorkspaceFile(path: string): path is `${string}/${typeof PNPM_WORKSPACE_BASENAME}` | `${string}/${typeof YARN_WORKSPACE_BASENAME}` {
19-
return path.endsWith(`/${PNPM_WORKSPACE_BASENAME}`)
20-
|| path.endsWith(`/${YARN_WORKSPACE_BASENAME}`)
21-
}

0 commit comments

Comments
 (0)