diff --git a/change/change-bun-workspace-tools.json b/change/change-bun-workspace-tools.json new file mode 100644 index 000000000..7a3941168 --- /dev/null +++ b/change/change-bun-workspace-tools.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "type": "patch", + "comment": "Add bun as a supported workspace manager in workspace-tools, including manager detection and workspace fixture/test coverage.", + "packageName": "workspace-tools", + "email": "email not defined", + "dependentChangeType": "patch" + } + ] +} diff --git a/packages/workspace-tools/README.md b/packages/workspace-tools/README.md index 55d215aef..f9eb6da4b 100644 --- a/packages/workspace-tools/README.md +++ b/packages/workspace-tools/README.md @@ -3,6 +3,7 @@ A collection of utilities that are useful in a git-controlled monorepo managed by one of these tools: - lerna +- bun workspaces - npm workspaces - pnpm workspaces - rush @@ -20,7 +21,7 @@ Override the `maxBuffer` value for git processes, for example if the repo is ver ### PREFERRED_WORKSPACE_MANAGER -Sometimes if multiple workspace/monorepo manager files are checked in, it's necessary to hint which manager is used: `npm`, `yarn`, `pnpm`, `rush`, or `lerna`. Some APIs also accept a `manager` parameter, which is now the preferred method when available. +Sometimes if multiple workspace/monorepo manager files are checked in, it's necessary to hint which manager is used: `bun`, `npm`, `yarn`, `pnpm`, `rush`, or `lerna`. Some APIs also accept a `manager` parameter, which is now the preferred method when available. ### VERBOSE diff --git a/packages/workspace-tools/etc/workspace-tools.api.md b/packages/workspace-tools/etc/workspace-tools.api.md index dab612d8e..68a688795 100644 --- a/packages/workspace-tools/etc/workspace-tools.api.md +++ b/packages/workspace-tools/etc/workspace-tools.api.md @@ -696,7 +696,7 @@ export function stageAndCommit(patterns: string[], message: string, cwd: string, export type WorkspaceInfos = WorkspacePackageInfo[]; // @public (undocumented) -export type WorkspaceManager = "yarn" | "pnpm" | "rush" | "npm" | "lerna"; +export type WorkspaceManager = "yarn" | "pnpm" | "rush" | "npm" | "lerna" | "bun"; // @public (undocumented) interface WorkspaceManagerAndRoot { diff --git a/packages/workspace-tools/src/__fixtures__/monorepo-basic-bun/README.md b/packages/workspace-tools/src/__fixtures__/monorepo-basic-bun/README.md new file mode 100644 index 000000000..7a1804142 --- /dev/null +++ b/packages/workspace-tools/src/__fixtures__/monorepo-basic-bun/README.md @@ -0,0 +1,5 @@ +This fixture is intended to match the other `monorepo-basic-*` fixtures (bun): + +- Workspaces: `["packages/*", "individual"]` +- Same basic dependencies at root +- `package-a` depends on `react` and `react-dom` (to introduce a `peerDependency`) diff --git a/packages/workspace-tools/src/__fixtures__/monorepo-basic-bun/bun.lock b/packages/workspace-tools/src/__fixtures__/monorepo-basic-bun/bun.lock new file mode 100644 index 000000000..e69de29bb diff --git a/packages/workspace-tools/src/__fixtures__/monorepo-basic-bun/individual/package.json b/packages/workspace-tools/src/__fixtures__/monorepo-basic-bun/individual/package.json new file mode 100644 index 000000000..ec1362d53 --- /dev/null +++ b/packages/workspace-tools/src/__fixtures__/monorepo-basic-bun/individual/package.json @@ -0,0 +1,5 @@ +{ + "name": "individual", + "license": "MIT", + "version": "0.1.0" +} diff --git a/packages/workspace-tools/src/__fixtures__/monorepo-basic-bun/package.json b/packages/workspace-tools/src/__fixtures__/monorepo-basic-bun/package.json new file mode 100644 index 000000000..8f9352fc9 --- /dev/null +++ b/packages/workspace-tools/src/__fixtures__/monorepo-basic-bun/package.json @@ -0,0 +1,17 @@ +{ + "name": "monorepo-basic-bun", + "description": "Basic monorepo with bun (workspaces and deps should match other monorepo-basic-*)", + "license": "MIT", + "version": "0.1.0", + "private": true, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^4.0.0" + }, + "workspaces": { + "packages": [ + "packages/*", + "individual" + ] + } +} diff --git a/packages/workspace-tools/src/__fixtures__/monorepo-basic-bun/packages/package-a/package.json b/packages/workspace-tools/src/__fixtures__/monorepo-basic-bun/packages/package-a/package.json new file mode 100644 index 000000000..1c3fb64f0 --- /dev/null +++ b/packages/workspace-tools/src/__fixtures__/monorepo-basic-bun/packages/package-a/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-a", + "license": "MIT", + "version": "0.1.0", + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + } +} diff --git a/packages/workspace-tools/src/__fixtures__/monorepo-basic-bun/packages/package-b/package.json b/packages/workspace-tools/src/__fixtures__/monorepo-basic-bun/packages/package-b/package.json new file mode 100644 index 000000000..8b6623a6e --- /dev/null +++ b/packages/workspace-tools/src/__fixtures__/monorepo-basic-bun/packages/package-b/package.json @@ -0,0 +1,5 @@ +{ + "name": "package-b", + "license": "MIT", + "version": "0.1.0" +} diff --git a/packages/workspace-tools/src/__tests__/setupFixture.ts b/packages/workspace-tools/src/__tests__/setupFixture.ts index 0016aa4d4..5031b127a 100644 --- a/packages/workspace-tools/src/__tests__/setupFixture.ts +++ b/packages/workspace-tools/src/__tests__/setupFixture.ts @@ -16,6 +16,7 @@ type RealFixtureName = | "extra-yarn-1" | "extra-yarn-berry" | "monorepo-basic-npm" + | "monorepo-basic-bun" | "monorepo-basic-pnpm" | "monorepo-basic-yarn-1" | "monorepo-basic-yarn-berry" diff --git a/packages/workspace-tools/src/__tests__/workspaces/getWorkspaceManagerAndRoot.test.ts b/packages/workspace-tools/src/__tests__/workspaces/getWorkspaceManagerAndRoot.test.ts index d76af6ffd..7ac7192af 100644 --- a/packages/workspace-tools/src/__tests__/workspaces/getWorkspaceManagerAndRoot.test.ts +++ b/packages/workspace-tools/src/__tests__/workspaces/getWorkspaceManagerAndRoot.test.ts @@ -21,6 +21,7 @@ describe("getWorkspaceManagerAndRoot", () => { { desc: "yarn", manager: "yarn", fixtureName: "monorepo-basic-yarn-1" }, { desc: "yarn berry", manager: "yarn", fixtureName: "monorepo-basic-yarn-berry" }, { desc: "pnpm", manager: "pnpm", fixtureName: "monorepo-basic-pnpm" }, + { desc: "bun", manager: "bun", fixtureName: "monorepo-basic-bun" }, { desc: "rush", manager: "rush", fixtureName: "monorepo-rush-pnpm" }, { desc: "npm", manager: "npm", fixtureName: "monorepo-basic-npm" }, { desc: "lerna + npm", manager: "lerna", fixtureName: "monorepo-basic-lerna-npm" }, @@ -44,6 +45,17 @@ describe("getWorkspaceManagerAndRoot", () => { }); }); + it("detects bun workspace root with bun.lockb", () => { + const repoRoot = setupFixture("monorepo-basic-bun"); + fs.rmSync(path.join(repoRoot, "bun.lock")); + fs.writeFileSync(path.join(repoRoot, "bun.lockb"), ""); + + expect(getWorkspaceManagerAndRoot(repoRoot)).toEqual({ + root: repoRoot, + manager: "bun", + }); + }); + it("handles nested monorepo", () => { // This fixture has a monorepo under the "monorepo" folder, not at the git root. const repoRoot = setupFixture("monorepo-nested", { git: true }); diff --git a/packages/workspace-tools/src/__tests__/workspaces/getWorkspaces.test.ts b/packages/workspace-tools/src/__tests__/workspaces/getWorkspaces.test.ts index a93a37aae..e6ada4ba5 100644 --- a/packages/workspace-tools/src/__tests__/workspaces/getWorkspaces.test.ts +++ b/packages/workspace-tools/src/__tests__/workspaces/getWorkspaces.test.ts @@ -17,6 +17,7 @@ describe("getWorkspaceInfos", () => { }>([ { manager: "yarn", desc: "yarn", fixtureName: "monorepo-basic-yarn-1" }, { manager: "pnpm", desc: "pnpm", fixtureName: "monorepo-basic-pnpm" }, + { manager: "bun", desc: "bun", fixtureName: "monorepo-basic-bun" }, { manager: "rush", desc: "rush + pnpm", fixtureName: "monorepo-rush-pnpm" }, { manager: "rush", desc: "rush + yarn", fixtureName: "monorepo-rush-yarn" }, { manager: "npm", desc: "npm", fixtureName: "monorepo-basic-npm" }, diff --git a/packages/workspace-tools/src/types/WorkspaceManager.ts b/packages/workspace-tools/src/types/WorkspaceManager.ts index ee41f0f17..fa299243a 100644 --- a/packages/workspace-tools/src/types/WorkspaceManager.ts +++ b/packages/workspace-tools/src/types/WorkspaceManager.ts @@ -1 +1 @@ -export type WorkspaceManager = "yarn" | "pnpm" | "rush" | "npm" | "lerna"; +export type WorkspaceManager = "yarn" | "pnpm" | "rush" | "npm" | "lerna" | "bun"; diff --git a/packages/workspace-tools/src/workspaces/implementations/bun.ts b/packages/workspace-tools/src/workspaces/implementations/bun.ts new file mode 100644 index 000000000..216c6aa42 --- /dev/null +++ b/packages/workspace-tools/src/workspaces/implementations/bun.ts @@ -0,0 +1,9 @@ +import { getPackageJsonWorkspacePatterns } from "./getPackageJsonWorkspacePatterns.js"; +import type { WorkspaceUtilities } from "./WorkspaceUtilities.js"; + +/** + * Bun uses the standard package.json "workspaces" field. + */ +export const bunUtilities: WorkspaceUtilities = { + getWorkspacePatterns: getPackageJsonWorkspacePatterns, +}; diff --git a/packages/workspace-tools/src/workspaces/implementations/getWorkspaceManagerAndRoot.ts b/packages/workspace-tools/src/workspaces/implementations/getWorkspaceManagerAndRoot.ts index bae8ae458..5c0731b8e 100644 --- a/packages/workspace-tools/src/workspaces/implementations/getWorkspaceManagerAndRoot.ts +++ b/packages/workspace-tools/src/workspaces/implementations/getWorkspaceManagerAndRoot.ts @@ -24,9 +24,15 @@ export const managerFiles = { rush: "rush.json", yarn: "yarn.lock", pnpm: "pnpm-workspace.yaml", + bun: ["bun.lock", "bun.lockb"], npm: "package-lock.json", } as const; +function getManagerFileNames(manager: WorkspaceManager): string[] { + const fileOrFiles = managerFiles[manager]; + return typeof fileOrFiles === "string" ? [fileOrFiles] : [...fileOrFiles]; +} + /** * Get the preferred workspace/monorepo manager based on `process.env.PREFERRED_WORKSPACE_MANAGER` * (if valid). @@ -57,7 +63,9 @@ export function getWorkspaceManagerAndRoot( } managerOverride ??= getPreferredWorkspaceManager(); - const filesToSearch = managerOverride ? managerFiles[managerOverride] : Object.values(managerFiles); + const filesToSearch = managerOverride + ? getManagerFileNames(managerOverride) + : Object.values(managerFiles).flatMap((files) => (Array.isArray(files) ? files : [files])); const managerFile = searchUp(filesToSearch, cwd); if (managerFile) { @@ -65,7 +73,7 @@ export function getWorkspaceManagerAndRoot( cache.set(cwd, { manager: managerOverride || - (Object.keys(managerFiles) as WorkspaceManager[]).find((name) => managerFiles[name] === managerFileName)!, + (Object.keys(managerFiles) as WorkspaceManager[]).find((name) => getManagerFileNames(name).includes(managerFileName))!, root: path.dirname(managerFile), }); } else { diff --git a/packages/workspace-tools/src/workspaces/implementations/getWorkspaceUtilities.ts b/packages/workspace-tools/src/workspaces/implementations/getWorkspaceUtilities.ts index bfa6138b9..61815d081 100644 --- a/packages/workspace-tools/src/workspaces/implementations/getWorkspaceUtilities.ts +++ b/packages/workspace-tools/src/workspaces/implementations/getWorkspaceUtilities.ts @@ -8,6 +8,11 @@ const utils: Partial> = {}; */ export function getWorkspaceUtilities(manager: WorkspaceManager): WorkspaceUtilities { switch (manager) { + case "bun": + // eslint-disable-next-line @typescript-eslint/consistent-type-imports, @typescript-eslint/no-require-imports + utils.bun ??= (require("./bun") as typeof import("./bun")).bunUtilities; + break; + case "npm": // eslint-disable-next-line @typescript-eslint/consistent-type-imports, @typescript-eslint/no-require-imports utils.npm ??= (require("./npm") as typeof import("./npm")).npmUtilities;