Skip to content

feat: support bun package manager and bun workspaces#106

Open
RYGRIT wants to merge 8 commits intonpmx-dev:mainfrom
RYGRIT:feat/bun
Open

feat: support bun package manager and bun workspaces#106
RYGRIT wants to merge 8 commits intonpmx-dev:mainfrom
RYGRIT:feat/bun

Conversation

@RYGRIT
Copy link
Copy Markdown
Collaborator

@RYGRIT RYGRIT commented Apr 14, 2026

related #102

Changes:

  • add Bun support to workspace context and dependency resolution
  • support catalog, catalogs, workspaces.catalog, etc in Bun workspaces
  • merge Bun root package.json manifest and catalog entries for hover/diagnostics
  • add tests covering Bun package manager, workspace catalogs, and Windows path behavior
  • update playground/bun to a real Bun workspace example

@RYGRIT RYGRIT marked this pull request as ready for review April 15, 2026 02:39
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 15, 2026

📝 Walkthrough

Walkthrough

Adds Bun support across the workspace and extraction layers: expands PackageManager to include bun; enhances JsonExtractor to implement WorkspaceCatalogExtractor and parse workspace catalog/catalogs; updates workspace path resolution and metadata handling to use posix joins and manager-specific workspace basenames; adjusts language-server workspace logic to detect package manager by folder URI and to merge manifest and workspace dependencies; introduces mergeResolvedDependencies; adds tests for JSON/catalog extraction, workspace behaviour and dependency merging; adds a Bun playground workspace and updates README docs to list npm, pnpm, yarn and bun for workspace-aware resolution.

Possibly related PRs

🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is directly related to the changeset, clearly explaining Bun support additions and catalog/workspace features.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/language-server/src/merge-resolved-dependencies.ts (1)

7-10: Return a fresh array in every non-undefined path for consistent immutability.

Current behaviour returns a copy when both inputs exist, but returns the original reference when only one exists. A consistent copy-on-return pattern is safer.

Proposed refactor
 export function mergeResolvedDependencies(
   manifestDependencies?: DependencyInfo[],
   workspaceDependencies?: DependencyInfo[],
 ): DependencyInfo[] | undefined {
-  if (manifestDependencies && workspaceDependencies)
-    return [...manifestDependencies, ...workspaceDependencies]
-
-  return manifestDependencies ?? workspaceDependencies
+  if (!manifestDependencies && !workspaceDependencies)
+    return undefined
+
+  return [
+    ...(manifestDependencies ?? []),
+    ...(workspaceDependencies ?? []),
+  ]
 }
packages/language-core/src/extractors/json.test.ts (1)

46-69: Add dependency assertions for the nested workspaces catalog case.

This test currently validates only catalogs. Asserting dependencies too would better guard against regressions in flattening logic.

Proposed test extension
   it('extracts catalogs nested inside the workspaces object', () => {
     const info = extractor.getWorkspaceCatalogInfo(`{
       "workspaces": {
         "packages": ["packages/*"],
         "catalog": {
           "react": "^19.0.0"
         },
         "catalogs": {
           "test": {
             "vitest": "^4.0.0"
           }
         }
       }
     }`)
 
     expect(info?.catalogs).toEqual({
       default: {
         react: '^19.0.0',
       },
       test: {
         vitest: '^4.0.0',
       },
     })
+    expect(info?.dependencies.map(({ rawName, rawSpec, categoryName }) => ({
+      rawName,
+      rawSpec,
+      categoryName,
+    }))).toEqual([
+      {
+        rawName: 'react',
+        rawSpec: '^19.0.0',
+        categoryName: '',
+      },
+      {
+        rawName: 'vitest',
+        rawSpec: '^4.0.0',
+        categoryName: 'test',
+      },
+    ])
   })

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ccbfa554-719f-48be-b1b2-85707c3dfbaf

📥 Commits

Reviewing files that changed from the base of the PR and between 3fe3c57 and cf58e9e.

📒 Files selected for processing (14)
  • README.md
  • extensions/vscode/README.md
  • packages/language-core/src/extractors/json.test.ts
  • packages/language-core/src/extractors/json.ts
  • packages/language-core/src/workspace.test.ts
  • packages/language-core/src/workspace.ts
  • packages/language-server/src/merge-resolved-dependencies.ts
  • packages/language-server/src/workspace.test.ts
  • packages/language-server/src/workspace.ts
  • playground/bun/.vscode/settings.json
  • playground/bun/package.json
  • playground/bun/packages/app/package.json
  • playground/bun/packages/utils/package.json
  • playground/playground.code-workspace

Comment thread packages/language-core/src/workspace.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/language-core/src/workspace.test.ts (1)

39-39: Consider renaming this test for accuracy.

The current title says “loads”, but the assertions verify the missing-file path returning undefined. A tighter name will reduce ambiguity when failures are scanned.

✏️ Suggested rename
-  it('still loads workspace catalogs for pnpm workspaces', async () => {
+  it('returns undefined when the pnpm workspace file is missing', async () => {

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 640385d0-80de-4858-b674-28c5899ef6a9

📥 Commits

Reviewing files that changed from the base of the PR and between cf58e9e and 18703ae.

📒 Files selected for processing (3)
  • packages/language-core/src/workspace.test.ts
  • packages/language-core/src/workspace.ts
  • packages/language-server/src/workspace.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/language-server/src/workspace.ts
  • packages/language-core/src/workspace.ts

Copy link
Copy Markdown
Member

@9romise 9romise left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your awesome work!

Comment thread packages/language-core/src/workspace.ts Outdated

const TRAILING_SLASHES_RE = /\/+$/

function joinUriPath(dir: string, basename: string): string {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a custom join instead of the pathe one? Are there some specific reasons?

Copy link
Copy Markdown
Collaborator Author

@RYGRIT RYGRIT Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom processing here is because on Windows, URI.path is /d:/repo/package.json, and if pathe.join() is used, it will be normalized to d:/repo/package.json, causing the string comparison with URI.path to fail.

// demo.ts
import { join } from 'pathe'
import { URI } from 'vscode-uri'

const uri = URI.file('D:/repo/playground/bun/package.json')

console.log('uri.toString():', uri.toString())
console.log('uri.path:', uri.path)

const rootPath = URI.file('D:/repo/playground/bun').path

console.log('rootPath:', rootPath)
console.log('pathe.join:', join(rootPath, 'package.json'))

function joinUriPath(dir: string, basename: string): string {
  return `${dir.replace(/\/+$/, '')}/${basename}`
}

console.log('joinUriPath:', joinUriPath(rootPath, 'package.json'))

console.log('pathe matches uri.path:', join(rootPath, 'package.json') === uri.path)
console.log('custom matches uri.path:', joinUriPath(rootPath, 'package.json') === uri.path)
uri.toString(): file:///d%3A/repo/playground/bun/package.json
uri.path: /D:/repo/playground/bun/package.json
rootPath: /D:/repo/playground/bun
pathe.join: D:/repo/playground/bun/package.json
joinUriPath: /D:/repo/playground/bun/package.json
pathe matches uri.path: false
custom matches uri.path: true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need to replace pathe with path-browserify. I've tried it locally and it works fine. Besides, it’s already a dependency of Volar, so this won’t increase the bundle size.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I’ll test path-browserify locally against the Windows URI path case (/d:/...) and push a follow-up commit if it preserves the expected behavior.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread packages/language-server/src/merge-resolved-dependencies.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
packages/language-server/src/workspace.ts (2)

163-178: Optional: parallelise the two loads for the Bun root manifest.

For the Bun case where uri.path === ctx.workspaceFilePath, loadPackageManifestInfo and loadWorkspaceFileInfo are independent and could run concurrently on the initial (un-cached) call:

♻️ Proposed change
-    const manifestDependencies = (await ctx.loadPackageManifestInfo(uri.path))?.dependencies
-    if (ctx.packageManager !== 'bun' || ctx.workspaceFilePath !== uri.path)
-      return manifestDependencies
-
-    const workspaceDependencies = (await ctx.loadWorkspaceFileInfo(uri.path))?.dependencies
-    return mergeResolvedDependencies(manifestDependencies, workspaceDependencies)
+    if (ctx.packageManager !== 'bun' || ctx.workspaceFilePath !== uri.path)
+      return (await ctx.loadPackageManifestInfo(uri.path))?.dependencies
+
+    const [manifestInfo, workspaceInfo] = await Promise.all([
+      ctx.loadPackageManifestInfo(uri.path),
+      ctx.loadWorkspaceFileInfo(uri.path),
+    ])
+    return mergeResolvedDependencies(manifestInfo?.dependencies, workspaceInfo?.dependencies)

40-49: The client-side handler already correctly supports the new URI format; consider removing the unused rootPath parameter from the WorkspaceAdapter interface.

The payload change from rootPath to folderUri.toString() is already handled correctly on the client side. The handler in extensions/vscode/src/request.ts (Line 18) calls Uri.parse(params.uri), which correctly expects and processes a full URI string rather than a bare path. No client-side changes are needed.

However, the WorkspaceAdapter interface in packages/language-core/src/workspace.ts (Line 32) still declares detectPackageManager: (rootPath: string) => Promise<PackageManager>, and the call site at Line 105 passes this.rootPath as an argument. The language-server implementation (Line 40 of packages/language-server/src/workspace.ts) omits this parameter entirely and never uses it. Remove the unused parameter from both the interface signature and the call site to clarify the actual contract.

packages/language-core/src/workspace.ts (1)

11-12: Finish the migration from pathe to path-browserify.

The previous review established that pathe.join normalises away the leading / of Windows URI paths (/D:/repo/...D:/repo/...), breaking equality with URI.path. Although join has been migrated, dirname on line 12 is still imported from pathe. It is used twice in findNearestPackageManifestPath (lines 178 and 189) where results are compared against this.rootPath via dir.startsWith(\${this.rootPath}/`). If pathe.dirname` applies the same normalisation, the walk will silently never match on Windows.

Consolidate both onto path-browserify to maintain consistent path semantics with URI.path throughout the file.

♻️ Proposed changes
-import path from 'path-browserify'
-import { dirname } from 'pathe'
+import path from 'path-browserify'
-    let dir = dirname(packageManifestPath)
+    let dir = path.posix.dirname(packageManifestPath)
-      const parent = dirname(dir)
+      const parent = path.posix.dirname(dir)

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6ed8f9b8-f770-4cc9-93e6-342f695427ba

📥 Commits

Reviewing files that changed from the base of the PR and between aa69f3c and a60abde.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • packages/language-core/package.json
  • packages/language-core/src/workspace.ts
  • packages/language-core/tsdown.config.ts
  • packages/language-server/src/workspace.test.ts
  • packages/language-server/src/workspace.ts
  • pnpm-workspace.yaml
✅ Files skipped from review due to trivial changes (3)
  • packages/language-core/tsdown.config.ts
  • pnpm-workspace.yaml
  • packages/language-server/src/workspace.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants