From 76430c6a5c5dc061a2c320c4c5c19fa6ca8f3136 Mon Sep 17 00:00:00 2001 From: tknickman Date: Wed, 13 Sep 2023 12:06:18 -0400 Subject: [PATCH 1/2] feat(create-turbo): support bun (beta) --- packages/create-turbo/__tests__/index.test.ts | 7 +- packages/create-turbo/__tests__/test-utils.ts | 2 +- .../src/commands/create/prompts.ts | 15 +- packages/create-turbo/src/transforms/types.ts | 4 +- .../__tests__/add-package-manager.test.ts | 5 +- .../get-turbo-upgrade-command.test.ts | 8 +- .../turbo-codemod/__tests__/migrate.test.ts | 7 + .../turbo-codemod/__tests__/test-utils.ts | 3 +- .../turbo-codemod/__tests__/transform.test.ts | 2 + .../migrate/steps/getTurboUpgradeCommand.ts | 12 +- packages/turbo-gen/__tests__/test-utils.ts | 4 +- packages/turbo-utils/src/index.ts | 1 - packages/turbo-utils/src/managers.ts | 15 +- packages/turbo-utils/src/types.ts | 2 + .../bun/monorepo/apps/docs/package.json | 11 + .../bun/monorepo/apps/web/package.json | 11 + .../__fixtures__/bun/monorepo/bun.lockb | Bin 0 -> 2235 bytes .../__fixtures__/bun/monorepo/package.json | 14 ++ .../monorepo/packages/tsconfig/package.json | 5 + .../bun/monorepo/packages/ui/package.json | 7 + .../__fixtures__/bun/non-monorepo/bun.lockb | Bin 0 -> 3506 bytes .../bun/non-monorepo/package.json | 10 + .../turbo-workspaces/__tests__/index.test.ts | 1 + .../__tests__/managers.test.ts | 9 + .../turbo-workspaces/__tests__/test-utils.ts | 4 +- .../turbo-workspaces/__tests__/utils.test.ts | 23 ++ .../src/commands/convert/index.ts | 16 +- packages/turbo-workspaces/src/convert.ts | 2 +- packages/turbo-workspaces/src/errors.ts | 1 + packages/turbo-workspaces/src/index.ts | 19 +- packages/turbo-workspaces/src/install.ts | 14 +- packages/turbo-workspaces/src/managers/bun.ts | 237 ++++++++++++++++++ .../turbo-workspaces/src/managers/index.ts | 5 +- .../turbo-workspaces/src/managers/pnpm.ts | 38 +-- packages/turbo-workspaces/src/types.ts | 4 +- .../src/updateDependencies.ts | 10 +- packages/turbo-workspaces/src/utils.ts | 53 +++- 37 files changed, 501 insertions(+), 80 deletions(-) create mode 100644 packages/turbo-workspaces/__fixtures__/bun/monorepo/apps/docs/package.json create mode 100644 packages/turbo-workspaces/__fixtures__/bun/monorepo/apps/web/package.json create mode 100755 packages/turbo-workspaces/__fixtures__/bun/monorepo/bun.lockb create mode 100644 packages/turbo-workspaces/__fixtures__/bun/monorepo/package.json create mode 100644 packages/turbo-workspaces/__fixtures__/bun/monorepo/packages/tsconfig/package.json create mode 100644 packages/turbo-workspaces/__fixtures__/bun/monorepo/packages/ui/package.json create mode 100755 packages/turbo-workspaces/__fixtures__/bun/non-monorepo/bun.lockb create mode 100644 packages/turbo-workspaces/__fixtures__/bun/non-monorepo/package.json create mode 100644 packages/turbo-workspaces/__tests__/utils.test.ts create mode 100644 packages/turbo-workspaces/src/managers/bun.ts diff --git a/packages/create-turbo/__tests__/index.test.ts b/packages/create-turbo/__tests__/index.test.ts index cb5313dba6f77..68d61147c0329 100644 --- a/packages/create-turbo/__tests__/index.test.ts +++ b/packages/create-turbo/__tests__/index.test.ts @@ -3,7 +3,7 @@ import childProcess from "node:child_process"; import chalk from "chalk"; import { setupTestFixtures, spyConsole, spyExit } from "@turbo/test-utils"; import { logger } from "@turbo/utils"; -import type { PackageManager } from "@turbo/workspaces"; +import type { PackageManager } from "@turbo/utils"; // imports for mocks import * as turboWorkspaces from "@turbo/workspaces"; import * as turboUtils from "@turbo/utils"; @@ -29,6 +29,7 @@ describe("create-turbo", () => { { packageManager: "yarn" }, { packageManager: "npm" }, { packageManager: "pnpm" }, + { packageManager: "bun" }, ])( "outputs expected console messages when using $packageManager", async ({ packageManager }) => { @@ -42,6 +43,7 @@ describe("create-turbo", () => { npm: "8.19.2", yarn: "1.22.10", pnpm: "7.22.2", + bun: "1.0.1", }); const mockCreateProject = jest @@ -98,7 +100,7 @@ describe("create-turbo", () => { } ); - test.only("throws correct error message when a download error is encountered", async () => { + test("throws correct error message when a download error is encountered", async () => { const { root } = useFixture({ fixture: `create-turbo` }); const packageManager = "pnpm"; const mockAvailablePackageManagers = jest @@ -107,6 +109,7 @@ describe("create-turbo", () => { npm: "8.19.2", yarn: "1.22.10", pnpm: "7.22.2", + bun: "1.0.1", }); const mockCreateProject = jest diff --git a/packages/create-turbo/__tests__/test-utils.ts b/packages/create-turbo/__tests__/test-utils.ts index fa6c20420e317..f5a4802cba4cd 100644 --- a/packages/create-turbo/__tests__/test-utils.ts +++ b/packages/create-turbo/__tests__/test-utils.ts @@ -1,5 +1,5 @@ import path from "path"; -import { PackageManager } from "@turbo/workspaces"; +import type { PackageManager } from "@turbo/utils"; export function getWorkspaceDetailsMockReturnValue({ root, diff --git a/packages/create-turbo/src/commands/create/prompts.ts b/packages/create-turbo/src/commands/create/prompts.ts index 7fc66b01770c0..1172b2ed23422 100644 --- a/packages/create-turbo/src/commands/create/prompts.ts +++ b/packages/create-turbo/src/commands/create/prompts.ts @@ -1,4 +1,4 @@ -import type { PackageManager } from "@turbo/workspaces"; +import type { PackageManager } from "@turbo/utils"; import { getAvailablePackageManagers, validateDirectory } from "@turbo/utils"; import inquirer from "inquirer"; import type { CreateCommandArgument } from "./types"; @@ -52,10 +52,15 @@ export async function packageManager({ // prompt for package manager if it wasn't provided as an argument, or if it was // provided, but isn't available (always allow npm) !manager || !availablePackageManagers[manager as PackageManager], - choices: ["npm", "pnpm", "yarn"].map((p) => ({ - name: p, - value: p, - disabled: availablePackageManagers[p as PackageManager] + choices: [ + { pm: "npm", label: "npm workspaces" }, + { pm: "pnpm", label: "pnpm workspaces" }, + { pm: "yarn", label: "yarn workspaces" }, + { pm: "bun", label: "bun workspaces (beta)" }, + ].map(({ pm, label }) => ({ + name: label, + value: pm, + disabled: availablePackageManagers[pm as PackageManager] ? false : `not installed`, })), diff --git a/packages/create-turbo/src/transforms/types.ts b/packages/create-turbo/src/transforms/types.ts index 8ca055e7a4a7a..cb28c10c20972 100644 --- a/packages/create-turbo/src/transforms/types.ts +++ b/packages/create-turbo/src/transforms/types.ts @@ -1,5 +1,5 @@ -import type { RepoInfo } from "@turbo/utils"; -import type { Project, PackageManager } from "@turbo/workspaces"; +import type { RepoInfo, PackageManager } from "@turbo/utils"; +import type { Project } from "@turbo/workspaces"; import type { CreateCommandOptions } from "../commands/create/types"; export interface TransformInput { diff --git a/packages/turbo-codemod/__tests__/add-package-manager.test.ts b/packages/turbo-codemod/__tests__/add-package-manager.test.ts index 7666e18463b71..98651a9eb6be5 100644 --- a/packages/turbo-codemod/__tests__/add-package-manager.test.ts +++ b/packages/turbo-codemod/__tests__/add-package-manager.test.ts @@ -16,7 +16,7 @@ interface TestCase { name: string; fixture: string; existingPackageManagerString: string | undefined; - packageManager: turboWorkspaces.PackageManager; + packageManager: turboUtils.PackageManager; packageManagerVersion: string; options: TransformerOptions; result: TransformerResults; @@ -153,6 +153,7 @@ describe("add-package-manager-2", () => { pnpm: packageManager === "pnpm" ? packageManagerVersion : undefined, npm: packageManager === "npm" ? packageManagerVersion : undefined, yarn: packageManager === "yarn" ? packageManagerVersion : undefined, + bun: packageManager === "bun" ? packageManagerVersion : undefined, }); const mockGetWorkspaceDetails = jest @@ -243,6 +244,7 @@ describe("add-package-manager-2", () => { pnpm: undefined, npm: undefined, yarn: undefined, + bun: undefined, }); const mockGetWorkspaceDetails = jest @@ -290,6 +292,7 @@ describe("add-package-manager-2", () => { pnpm: packageManagerVersion, npm: undefined, yarn: undefined, + bun: undefined, }); const mockGetWorkspaceDetails = jest diff --git a/packages/turbo-codemod/__tests__/get-turbo-upgrade-command.test.ts b/packages/turbo-codemod/__tests__/get-turbo-upgrade-command.test.ts index a52a06c78d9c3..8bbee953f8c59 100644 --- a/packages/turbo-codemod/__tests__/get-turbo-upgrade-command.test.ts +++ b/packages/turbo-codemod/__tests__/get-turbo-upgrade-command.test.ts @@ -12,7 +12,7 @@ jest.mock("@turbo/workspaces", () => ({ interface TestCase { version: string; - packageManager: turboWorkspaces.PackageManager; + packageManager: turboUtils.PackageManager; packageManagerVersion: string; fixture: string; expected: string; @@ -457,6 +457,7 @@ describe("get-turbo-upgrade-command", () => { pnpm: undefined, npm: undefined, yarn: undefined, + bun: undefined, }); const mockGetAvailablePackageManagers = jest .spyOn(turboUtils, "getAvailablePackageManagers") @@ -464,6 +465,7 @@ describe("get-turbo-upgrade-command", () => { pnpm: packageManager === "pnpm" ? packageManagerVersion : undefined, npm: packageManager === "npm" ? packageManagerVersion : undefined, yarn: packageManager === "yarn" ? packageManagerVersion : undefined, + bun: packageManager === "bun" ? packageManagerVersion : undefined, }); const project = getWorkspaceDetailsMockReturnValue({ @@ -517,6 +519,7 @@ describe("get-turbo-upgrade-command", () => { pnpm: `/global/pnpm/bin`, npm: `/global/npm/bin`, yarn: `/global/yarn/bin`, + bun: `/global/bun/bin`, }); const mockGetAvailablePackageManagers = jest @@ -525,6 +528,7 @@ describe("get-turbo-upgrade-command", () => { pnpm: packageManager === "pnpm" ? packageManagerVersion : undefined, npm: packageManager === "npm" ? packageManagerVersion : undefined, yarn: packageManager === "yarn" ? packageManagerVersion : undefined, + bun: packageManager === "bun" ? packageManagerVersion : undefined, }); const project = getWorkspaceDetailsMockReturnValue({ @@ -571,6 +575,7 @@ describe("get-turbo-upgrade-command", () => { pnpm: "8.0.0", npm: undefined, yarn: undefined, + bun: undefined, }); const project = getWorkspaceDetailsMockReturnValue({ @@ -626,6 +631,7 @@ describe("get-turbo-upgrade-command", () => { pnpm: "8.0.0", npm: undefined, yarn: undefined, + bun: undefined, }); const project = getWorkspaceDetailsMockReturnValue({ diff --git a/packages/turbo-codemod/__tests__/migrate.test.ts b/packages/turbo-codemod/__tests__/migrate.test.ts index 8be5282e70b5d..1a6171b5e9d62 100644 --- a/packages/turbo-codemod/__tests__/migrate.test.ts +++ b/packages/turbo-codemod/__tests__/migrate.test.ts @@ -48,6 +48,7 @@ describe("migrate", () => { pnpm: packageManagerVersion, npm: undefined, yarn: undefined, + bun: undefined, }); const mockedGetWorkspaceDetails = jest .spyOn(turboWorkspaces, "getWorkspaceDetails") @@ -134,6 +135,7 @@ describe("migrate", () => { pnpm: packageManagerVersion, npm: undefined, yarn: undefined, + bun: undefined, }); const mockedGetWorkspaceDetails = jest .spyOn(turboWorkspaces, "getWorkspaceDetails") @@ -202,6 +204,7 @@ describe("migrate", () => { pnpm: packageManagerVersion, npm: undefined, yarn: undefined, + bun: undefined, }); const mockedGetWorkspaceDetails = jest .spyOn(turboWorkspaces, "getWorkspaceDetails") @@ -286,6 +289,7 @@ describe("migrate", () => { pnpm: packageManagerVersion, npm: undefined, yarn: undefined, + bun: undefined, }); const mockedGetWorkspaceDetails = jest .spyOn(turboWorkspaces, "getWorkspaceDetails") @@ -489,6 +493,7 @@ describe("migrate", () => { pnpm: packageManagerVersion, npm: undefined, yarn: undefined, + bun: undefined, }); const mockedGetWorkspaceDetails = jest .spyOn(turboWorkspaces, "getWorkspaceDetails") @@ -596,6 +601,7 @@ describe("migrate", () => { pnpm: packageManagerVersion, npm: undefined, yarn: undefined, + bun: undefined, }); const mockedGetWorkspaceDetails = jest .spyOn(turboWorkspaces, "getWorkspaceDetails") @@ -832,6 +838,7 @@ describe("migrate", () => { pnpm: packageManagerVersion, npm: undefined, yarn: undefined, + bun: undefined, }); const mockedGetWorkspaceDetails = jest .spyOn(turboWorkspaces, "getWorkspaceDetails") diff --git a/packages/turbo-codemod/__tests__/test-utils.ts b/packages/turbo-codemod/__tests__/test-utils.ts index ded87bb366355..0e065d5a1c187 100644 --- a/packages/turbo-codemod/__tests__/test-utils.ts +++ b/packages/turbo-codemod/__tests__/test-utils.ts @@ -1,5 +1,6 @@ import path from "node:path"; -import type { PackageManager, Project } from "@turbo/workspaces"; +import type { PackageManager } from "@turbo/utils"; +import type { Project } from "@turbo/workspaces"; export function getWorkspaceDetailsMockReturnValue({ root, diff --git a/packages/turbo-codemod/__tests__/transform.test.ts b/packages/turbo-codemod/__tests__/transform.test.ts index 1a346199f7c4e..01953e4037434 100644 --- a/packages/turbo-codemod/__tests__/transform.test.ts +++ b/packages/turbo-codemod/__tests__/transform.test.ts @@ -36,6 +36,7 @@ describe("transform", () => { pnpm: packageManagerVersion, npm: undefined, yarn: undefined, + bun: undefined, }); const mockGetWorkspaceDetails = jest @@ -93,6 +94,7 @@ describe("transform", () => { pnpm: packageManagerVersion, npm: undefined, yarn: undefined, + bun: undefined, }); const mockGetWorkspaceDetails = jest diff --git a/packages/turbo-codemod/src/commands/migrate/steps/getTurboUpgradeCommand.ts b/packages/turbo-codemod/src/commands/migrate/steps/getTurboUpgradeCommand.ts index 411141a060a98..3fe6666a4d252 100644 --- a/packages/turbo-codemod/src/commands/migrate/steps/getTurboUpgradeCommand.ts +++ b/packages/turbo-codemod/src/commands/migrate/steps/getTurboUpgradeCommand.ts @@ -5,9 +5,10 @@ import { getAvailablePackageManagers, getPackageManagersBinPaths, logger, + type PackageManager, type PackageJson, } from "@turbo/utils"; -import { type Project, type PackageManager } from "@turbo/workspaces"; +import type { Project } from "@turbo/workspaces"; import { exec } from "../utils"; type InstallType = "dependencies" | "devDependencies"; @@ -23,6 +24,8 @@ function getGlobalUpgradeCommand( return `npm install turbo@${to} --global`; case "pnpm": return `pnpm add turbo@${to} --global`; + case "bun": + return `bun add turbo@${to} --global`; } } @@ -78,6 +81,13 @@ function getLocalUpgradeCommand({ installType === "devDependencies" && "--save-dev", isUsingWorkspaces && "-w", ]); + case "bun": + return renderCommand([ + "bun", + "add", + `turbo@${to}`, + installType === "devDependencies" && "--dev", + ]); } } diff --git a/packages/turbo-gen/__tests__/test-utils.ts b/packages/turbo-gen/__tests__/test-utils.ts index fa6c20420e317..f594a42c179e2 100644 --- a/packages/turbo-gen/__tests__/test-utils.ts +++ b/packages/turbo-gen/__tests__/test-utils.ts @@ -1,5 +1,5 @@ -import path from "path"; -import { PackageManager } from "@turbo/workspaces"; +import path from "node:path"; +import type { PackageManager } from "@turbo/utils"; export function getWorkspaceDetailsMockReturnValue({ root, diff --git a/packages/turbo-utils/src/index.ts b/packages/turbo-utils/src/index.ts index b6f5d3970530d..0908872cd35b5 100644 --- a/packages/turbo-utils/src/index.ts +++ b/packages/turbo-utils/src/index.ts @@ -23,7 +23,6 @@ export { convertCase } from "./convertCase"; export * as logger from "./logger"; // types -export type { PackageManagerAvailable } from "./managers"; export type { RepoInfo } from "./examples"; export type { TurboConfig, diff --git a/packages/turbo-utils/src/managers.ts b/packages/turbo-utils/src/managers.ts index dc0ddcaac9b13..5591ae0f75d93 100644 --- a/packages/turbo-utils/src/managers.ts +++ b/packages/turbo-utils/src/managers.ts @@ -1,12 +1,7 @@ import os from "node:os"; import type { Options } from "execa"; import execa from "execa"; - -export type PackageManager = "npm" | "yarn" | "pnpm"; -export interface PackageManagerAvailable { - available: boolean; - version?: string; -} +import type { PackageManager } from "./types"; async function exec(command: string, args: Array = [], opts?: Options) { // run the check from tmpdir to avoid corepack conflicting - @@ -28,31 +23,35 @@ async function exec(command: string, args: Array = [], opts?: Options) { export async function getAvailablePackageManagers(): Promise< Record > { - const [yarn, npm, pnpm] = await Promise.all([ + const [yarn, npm, pnpm, bun] = await Promise.all([ exec("yarnpkg", ["--version"]), exec("npm", ["--version"]), exec("pnpm", ["--version"]), + exec("bun", ["--version"]), ]); return { yarn, pnpm, npm, + bun, }; } export async function getPackageManagersBinPaths(): Promise< Record > { - const [yarn, npm, pnpm] = await Promise.all([ + const [yarn, npm, pnpm, bun] = await Promise.all([ exec("yarnpkg", ["global", "bin"]), exec("npm", ["config", "get", "prefix"]), exec("pnpm", ["bin", "--global"]), + exec("bun", ["pm", "--g", "bin"]), ]); return { yarn, pnpm, npm, + bun, }; } diff --git a/packages/turbo-utils/src/types.ts b/packages/turbo-utils/src/types.ts index 43d8987e84a57..b11bf2b245ace 100644 --- a/packages/turbo-utils/src/types.ts +++ b/packages/turbo-utils/src/types.ts @@ -1,5 +1,7 @@ import type { Schema } from "@turbo/types"; +export type PackageManager = "npm" | "yarn" | "pnpm" | "bun"; + export type DependencyList = Record; export interface DependencyGroups { diff --git a/packages/turbo-workspaces/__fixtures__/bun/monorepo/apps/docs/package.json b/packages/turbo-workspaces/__fixtures__/bun/monorepo/apps/docs/package.json new file mode 100644 index 0000000000000..d3a490ca892f8 --- /dev/null +++ b/packages/turbo-workspaces/__fixtures__/bun/monorepo/apps/docs/package.json @@ -0,0 +1,11 @@ +{ + "name": "docs", + "version": "0.0.0", + "private": true, + "dependencies": { + "ui": "*" + }, + "devDependencies": { + "tsconfig": "*" + } +} diff --git a/packages/turbo-workspaces/__fixtures__/bun/monorepo/apps/web/package.json b/packages/turbo-workspaces/__fixtures__/bun/monorepo/apps/web/package.json new file mode 100644 index 0000000000000..163cee3e51e6c --- /dev/null +++ b/packages/turbo-workspaces/__fixtures__/bun/monorepo/apps/web/package.json @@ -0,0 +1,11 @@ +{ + "name": "web", + "version": "0.0.0", + "private": true, + "dependencies": { + "ui": "*" + }, + "devDependencies": { + "tsconfig": "*" + } +} diff --git a/packages/turbo-workspaces/__fixtures__/bun/monorepo/bun.lockb b/packages/turbo-workspaces/__fixtures__/bun/monorepo/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..11dacfe2b9a15eefadebfa9492b4d74360c48491 GIT binary patch literal 2235 zcmY#Z)GsYA(of3F(@)JSQ%EY!;{sycoc!eMw9K4T-L(9o+{6;yG6OCq1_p-7I-6W9 zP8@6Y`d#tdWmk0Cui4Ud`+ZZVIMoEV#GUoC)8+sw0s>YD#Q^2P=?18HAxr_7&&R;f zP@b9uW|w9{l#~=F=jWwmrl;g57lTA%X3U)OeQ~L3+*8(5U%E7VB95-uP#s{=e(7X> z(c7HsLd(w2`5oExwTuy>onZo$-oYf2F&}8#Dky*ooDK^?0o62+t6vAYF2GKC~1gKe6(87buCfdj>w$uXX zFEhBC3{m|BEAL_DxdI!kB!krmuo7Q^4OT|N>I_(&puh$zePOi=14.0.0" + }, + "dependencies": {}, + "packageManager": "bun@1.0.1" +} diff --git a/packages/turbo-workspaces/__fixtures__/bun/monorepo/packages/tsconfig/package.json b/packages/turbo-workspaces/__fixtures__/bun/monorepo/packages/tsconfig/package.json new file mode 100644 index 0000000000000..3f406290b4667 --- /dev/null +++ b/packages/turbo-workspaces/__fixtures__/bun/monorepo/packages/tsconfig/package.json @@ -0,0 +1,5 @@ +{ + "name": "tsconfig", + "version": "0.0.0", + "private": true +} diff --git a/packages/turbo-workspaces/__fixtures__/bun/monorepo/packages/ui/package.json b/packages/turbo-workspaces/__fixtures__/bun/monorepo/packages/ui/package.json new file mode 100644 index 0000000000000..1e05d1a8ecc65 --- /dev/null +++ b/packages/turbo-workspaces/__fixtures__/bun/monorepo/packages/ui/package.json @@ -0,0 +1,7 @@ +{ + "name": "ui", + "version": "0.0.0", + "devDependencies": { + "tsconfig": "*" + } +} diff --git a/packages/turbo-workspaces/__fixtures__/bun/non-monorepo/bun.lockb b/packages/turbo-workspaces/__fixtures__/bun/non-monorepo/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..69d76bb9ab0d6034a786c2fdc1a3abe6f6158e95 GIT binary patch literal 3506 zcmY#Z)GsYA(of3F(@)JSQ%EY!;{sycoc!eMw9K4T-L(9o+{6;yG6OCq1_lOexn&z2 z7c4aBDxD>D{JVS6>76EtEj!yzZ|Z+J{e@4yr4lbt5fE@dC=9v*DxLvTzyP!iELKul zl#~x-^#ZXl14BbCkQM;aIY3$vNXG$bJ|OJ_q=kUAEs*91(gzyvtvq|r`pV-gj|Bb* zh9q=XvCO#QW-Kf8GH9i1&GZN<&oJ>+rpuFV^4pw0^nmqjf<1$^^!aQhohkQ-_bI(kqVQ@{NJa zC)kYt>6e<;>YId*i0=vNjL4waX7Q|JWR2;=EQo&++KvTj#mU?s&@mexB2>2+ZH0 zFyv?e(#${%11#QCKY5B@yTQ5K_nE1peP`Lewngin9nFxM8yLSXYs#-xtM0tqKW{JF z#G~RrmYi68vwzK9=6mx3!W06}y)0m#u`Up35Xim2as)^s0Lvu>uYD?Qj+}`%UfgVu zX6tjlzF7LAveavxZKo~wa4xRq>uLNlt#MgM$1+Jhm;1SI794-{uYXPFjVHHRO(%pO zehW62fdgbNE5cX?mJ2#FW))4|yi(@){!W+L=QdtV^G)aOIIGxyu>bEW+15oWI#(Jb z*!H=pNuFP@Td}zAsB7Z0UDy0d&FVvzDC-?71DneNb}J|?!T?LfDNgQ})(tH?y0f}p zZCbXbh0T8H>Tksr3-5exC`@nIa;7Uw<>tX|?W);o=lq_;UuU`~=fb(}=#%ubJi^m| ztOJE9D11R-087^(vsw0KK9Kyjtt?DGek*HOxsHjko`35%jX(YBNy)#Z??0=)Fn3D* zo%Uvd%s9#Dp6(xkXKyTDEF->()3z>9jj^jF9%Kj>07@IM@*E@$EE^j@^gxM$>?Rf< z=NDr2jH)9X0vn(;{t+n6Wm8;~te2TrT#}fRqX%nl=!F!errIeO87LHIR;A{r=_r^e zB<5tM=jEq?Dpe@>_a6d4Tu^%c05y{bR0#tCmrbd$ksVMg4nsh33`(P*d;!W6pgau9 zZx&GfAw-x1G6Q5cNFPW)$Q+P)AagsQ`t6`@=CX-4GK(#>0D8;}?j}Q2kHPW=tR2I_ zl#^JJT3o`w0Bh;MS}ZJ#hI)nudWOcJ`WoC8g0*Q7GO*SXtkr{%$pPvCwWnb1B7_XA z#RY3QA!J}}GFaORA=3f%A&f?i)#B9LveY6y*NT$VqP)bM;F6-uymUJSLqs50zyo0i z)Fdfr!wDFN!0m`F}$UTFnf9>PPC25L^pFE55Gfbqdf2suj^ zMK#=!D5A(tCfYnR6AZ)5Oh_^b$)j+iki^I^3&~BeFeS+pXiR|v7VacuL2^t(auhrz MAe#fsD1#CJ05LtO!~g&Q literal 0 HcmV?d00001 diff --git a/packages/turbo-workspaces/__fixtures__/bun/non-monorepo/package.json b/packages/turbo-workspaces/__fixtures__/bun/non-monorepo/package.json new file mode 100644 index 0000000000000..3d4712b9a34c8 --- /dev/null +++ b/packages/turbo-workspaces/__fixtures__/bun/non-monorepo/package.json @@ -0,0 +1,10 @@ +{ + "name": "bun", + "version": "0.0.0", + "private": true, + "dependencies": {}, + "devDependencies": { + "turbo": "latest" + }, + "packageManager": "bun@1.0.1" +} diff --git a/packages/turbo-workspaces/__tests__/index.test.ts b/packages/turbo-workspaces/__tests__/index.test.ts index b05fd503a6492..623ea72aed569 100644 --- a/packages/turbo-workspaces/__tests__/index.test.ts +++ b/packages/turbo-workspaces/__tests__/index.test.ts @@ -29,6 +29,7 @@ describe("Node entrypoint", () => { npm: "8.19.2", yarn: "1.22.19", pnpm: "7.29.1", + bun: "1.0.1", }); const { root } = useFixture({ diff --git a/packages/turbo-workspaces/__tests__/managers.test.ts b/packages/turbo-workspaces/__tests__/managers.test.ts index 26ac1f0d295b5..734b42d2fa6cd 100644 --- a/packages/turbo-workspaces/__tests__/managers.test.ts +++ b/packages/turbo-workspaces/__tests__/managers.test.ts @@ -168,6 +168,8 @@ describe("managers", () => { await expect(read).rejects.toThrow(`Not a pnpm project`); } else if (toManager === "yarn") { await expect(read).rejects.toThrow(`Not a yarn project`); + } else if (toManager === "bun") { + await expect(read).rejects.toThrow(`Not a bun project`); } else { await expect(read).rejects.toThrow(`Not an npm project`); } @@ -194,6 +196,8 @@ describe("managers", () => { new RegExp(`^.*\/${directoryName}\/yarn.lock$`); } else if (fixtureManager === "npm") { new RegExp(`^.*\/${directoryName}\/package-lock.json$`); + } else if (fixtureManager === "bun") { + new RegExp(`^.*\/${directoryName}\/bun.lockb$`); } else { throw new Error("Invalid fixtureManager"); } @@ -246,6 +250,8 @@ describe("managers", () => { await expect(read).rejects.toThrow(`Not a pnpm project`); } else if (toManager === "yarn") { await expect(read).rejects.toThrow(`Not a yarn project`); + } else if (toManager === "bun") { + await expect(read).rejects.toThrow(`Not a bun project`); } else { await expect(read).rejects.toThrow(`Not an npm project`); } @@ -272,6 +278,8 @@ describe("managers", () => { new RegExp(`^.*\/${directoryName}\/yarn.lock$`); } else if (fixtureManager === "npm") { new RegExp(`^.*\/${directoryName}\/package-lock.json$`); + } else if (fixtureManager === "bun") { + new RegExp(`^.*\/${directoryName}\/bun.lockb$`); } else { throw new Error("Invalid fixtureManager"); } @@ -343,6 +351,7 @@ describe("managers", () => { await MANAGERS[toManager].convertLock({ project, + to: { name: toManager, version: "1.2.3" }, logger: new Logger(), options: { interactive, diff --git a/packages/turbo-workspaces/__tests__/test-utils.ts b/packages/turbo-workspaces/__tests__/test-utils.ts index 7943e783e96c7..2bf2d2f21b3e9 100644 --- a/packages/turbo-workspaces/__tests__/test-utils.ts +++ b/packages/turbo-workspaces/__tests__/test-utils.ts @@ -1,6 +1,6 @@ -import type { PackageManager } from "../src/types"; +import type { PackageManager } from "@turbo/utils"; -const PACKAGE_MANAGERS: Array = ["pnpm", "npm", "yarn"]; +const PACKAGE_MANAGERS: Array = ["pnpm", "npm", "yarn", "bun"]; const REPO_TYPES: Array<"monorepo" | "non-monorepo"> = [ "monorepo", "non-monorepo", diff --git a/packages/turbo-workspaces/__tests__/utils.test.ts b/packages/turbo-workspaces/__tests__/utils.test.ts new file mode 100644 index 0000000000000..4c9ec32dbdccd --- /dev/null +++ b/packages/turbo-workspaces/__tests__/utils.test.ts @@ -0,0 +1,23 @@ +import type { Project } from "../src/types"; +import { isCompatibleWithBunWorkspace } from "../src/utils"; + +describe("utils", () => { + describe("isCompatibleWithBunWorkspace", () => { + test.each([ + { globs: ["apps/*"], expected: true }, + { globs: ["apps/*", "packages/*"], expected: true }, + { globs: ["*"], expected: true }, + { globs: ["workspaces/**/*"], expected: false }, + { globs: ["apps/*", "packages/**/*"], expected: false }, + { globs: ["apps/*", "packages/*/utils/*"], expected: false }, + { globs: ["internal-*/*"], expected: false }, + ])("should return $result when given %globs", ({ globs, expected }) => { + const result = isCompatibleWithBunWorkspace({ + project: { + workspaceData: { globs }, + } as Project, + }); + expect(result).toEqual(expected); + }); + }); +}); diff --git a/packages/turbo-workspaces/src/commands/convert/index.ts b/packages/turbo-workspaces/src/commands/convert/index.ts index 281f4ef814d4b..ee6c6f669c6d6 100644 --- a/packages/turbo-workspaces/src/commands/convert/index.ts +++ b/packages/turbo-workspaces/src/commands/convert/index.ts @@ -1,10 +1,9 @@ import inquirer from "inquirer"; import chalk from "chalk"; -import { getAvailablePackageManagers } from "@turbo/utils"; +import { getAvailablePackageManagers, type PackageManager } from "@turbo/utils"; import { Logger } from "../../logger"; import { directoryInfo } from "../../utils"; import { getWorkspaceDetails } from "../../getWorkspaceDetails"; -import type { PackageManager } from "../../types"; import { convertProject } from "../../convert"; import type { ConvertCommandArgument, ConvertCommandOptions } from "./types"; @@ -80,11 +79,16 @@ export async function convertCommand( when: !packageManager || !Object.keys(availablePackageManagers).includes(packageManager), - choices: ["npm", "pnpm", "yarn"].map((p) => ({ - name: `${p} workspaces`, - value: p, + choices: [ + { pm: "npm", label: "npm workspaces" }, + { pm: "pnpm", label: "pnpm workspaces" }, + { pm: "yarn", label: "yarn workspaces" }, + { pm: "bun", label: "bun workspaces (beta)" }, + ].map(({ pm, label }) => ({ + name: label, + value: pm, disabled: isPackageManagerDisabled({ - packageManager: p as PackageManager, + packageManager: pm as PackageManager, currentWorkspaceManger: project.packageManager, availablePackageManagers, }), diff --git a/packages/turbo-workspaces/src/convert.ts b/packages/turbo-workspaces/src/convert.ts index 273e9397e25bc..b4d56eae7ce28 100644 --- a/packages/turbo-workspaces/src/convert.ts +++ b/packages/turbo-workspaces/src/convert.ts @@ -67,7 +67,7 @@ export async function convertProject({ logger.mainStep("Installing dependencies"); if (!options?.skipInstall) { - await MANAGERS[to.name].convertLock({ project, logger, options }); + await MANAGERS[to.name].convertLock({ project, to, logger, options }); await install({ project, to, logger, options }); } else { logger.subStep(chalk.yellow("Skipping install")); diff --git a/packages/turbo-workspaces/src/errors.ts b/packages/turbo-workspaces/src/errors.ts index be71437cf859e..74be76b59d836 100644 --- a/packages/turbo-workspaces/src/errors.ts +++ b/packages/turbo-workspaces/src/errors.ts @@ -7,6 +7,7 @@ export type ConvertErrorType = | "package_manager-could_not_be_found" // package manager specific | "pnpm-workspace_parse_error" + | "bun-workspace_glob_error" // package.json | "package_json-parse_error" | "package_json-missing" diff --git a/packages/turbo-workspaces/src/index.ts b/packages/turbo-workspaces/src/index.ts index 557723c75f7f3..c9376ebfe08fe 100644 --- a/packages/turbo-workspaces/src/index.ts +++ b/packages/turbo-workspaces/src/index.ts @@ -1,17 +1,11 @@ -import { getAvailablePackageManagers } from "@turbo/utils"; +import { getAvailablePackageManagers, type PackageManager } from "@turbo/utils"; import { getWorkspaceDetails } from "./getWorkspaceDetails"; import { convertProject } from "./convert"; import { Logger } from "./logger"; import { install, getPackageManagerMeta } from "./install"; import { ConvertError } from "./errors"; import { MANAGERS } from "./managers"; -import type { - PackageManager, - Options, - InstallArgs, - Workspace, - Project, -} from "./types"; +import type { Options, InstallArgs, Workspace, Project } from "./types"; import type { ConvertErrorType } from "./errors"; async function convert({ @@ -39,14 +33,7 @@ async function convert({ }); } -export type { - PackageManager, - Options, - InstallArgs, - Workspace, - Project, - ConvertErrorType, -}; +export type { Options, InstallArgs, Workspace, Project, ConvertErrorType }; export { convert, getWorkspaceDetails, diff --git a/packages/turbo-workspaces/src/install.ts b/packages/turbo-workspaces/src/install.ts index 3204f861629ba..67eb21faabdfa 100644 --- a/packages/turbo-workspaces/src/install.ts +++ b/packages/turbo-workspaces/src/install.ts @@ -1,10 +1,10 @@ +import type { PackageManager } from "@turbo/utils"; import execa from "execa"; import ora from "ora"; import { satisfies } from "semver"; import { ConvertError } from "./errors"; import { Logger } from "./logger"; import type { - PackageManager, RequestedPackageManagerDetails, PackageManagerInstallDetails, InstallArgs, @@ -68,6 +68,18 @@ export const PACKAGE_MANAGERS: Record< semver: ">=2", }, ], + bun: [ + { + name: "bun1", + template: "bun", + command: "bun", + installArgs: ["install"], + version: "1.x", + executable: "bunx", + semver: "<2", + default: true, + }, + ], }; export function getPackageManagerMeta( diff --git a/packages/turbo-workspaces/src/managers/bun.ts b/packages/turbo-workspaces/src/managers/bun.ts new file mode 100644 index 0000000000000..e5289d70cdd3d --- /dev/null +++ b/packages/turbo-workspaces/src/managers/bun.ts @@ -0,0 +1,237 @@ +import path from "node:path"; +import { existsSync, writeJSONSync, rmSync, rm } from "fs-extra"; +import { ConvertError } from "../errors"; +import { updateDependencies } from "../updateDependencies"; +import type { + DetectArgs, + ReadArgs, + CreateArgs, + RemoveArgs, + ConvertArgs, + CleanArgs, + Project, + ManagerHandler, +} from "../types"; +import { + getMainStep, + getWorkspaceInfo, + getPackageJson, + expandPaths, + expandWorkspaces, + getWorkspacePackageManager, + parseWorkspacePackages, + isCompatibleWithBunWorkspace, +} from "../utils"; + +/** + * Check if a given project is using bun workspaces + * Verify by checking for the existence of: + * 1. bun.lockb + * 2. packageManager field in package.json + */ +// eslint-disable-next-line @typescript-eslint/require-await -- must match the detect type signature +async function detect(args: DetectArgs): Promise { + const lockFile = path.join(args.workspaceRoot, "bun.lockb"); + const packageManager = getWorkspacePackageManager({ + workspaceRoot: args.workspaceRoot, + }); + return existsSync(lockFile) || packageManager === "bun"; +} + +/** + Read workspace data from bun workspaces into generic format +*/ +async function read(args: ReadArgs): Promise { + const isBun = await detect(args); + if (!isBun) { + throw new ConvertError("Not a bun project", { + type: "package_manager-unexpected", + }); + } + + const packageJson = getPackageJson(args); + const { name, description } = getWorkspaceInfo(args); + const workspaceGlobs = parseWorkspacePackages({ + workspaces: packageJson.workspaces, + }); + return { + name, + description, + packageManager: "bun", + paths: expandPaths({ + root: args.workspaceRoot, + lockFile: "bun.lockb", + }), + workspaceData: { + globs: workspaceGlobs, + workspaces: expandWorkspaces({ + workspaceGlobs, + ...args, + }), + }, + }; +} + +/** + * Create bun workspaces from generic format + * + * Creating bun workspaces involves: + * 1. Validating that the project can be converted to bun workspace + * 2. Adding the workspaces field in package.json + * 3. Setting the packageManager field in package.json + * 4. Updating all workspace package.json dependencies to ensure correct format + */ +// eslint-disable-next-line @typescript-eslint/require-await -- must match the create type signature +async function create(args: CreateArgs): Promise { + const { project, to, logger, options } = args; + const hasWorkspaces = project.workspaceData.globs.length > 0; + + if (!isCompatibleWithBunWorkspace({ project })) { + throw new ConvertError( + "Unable to convert project to bun - workspace globs unsupported", + { + type: "bun-workspace_glob_error", + } + ); + } + + logger.mainStep( + getMainStep({ packageManager: "bun", action: "create", project }) + ); + const packageJson = getPackageJson({ workspaceRoot: project.paths.root }); + logger.rootHeader(); + + // package manager + logger.rootStep( + `adding "packageManager" field to ${path.relative( + project.paths.root, + project.paths.packageJson + )}` + ); + // TODO: This technically isn't valid as part of the spec (yet) + packageJson.packageManager = `${to.name}@${to.version}`; + + if (hasWorkspaces) { + // workspaces field + logger.rootStep( + `adding "workspaces" field to ${path.relative( + project.paths.root, + project.paths.packageJson + )}` + ); + packageJson.workspaces = project.workspaceData.globs; + + if (!options?.dry) { + writeJSONSync(project.paths.packageJson, packageJson, { spaces: 2 }); + } + + // root dependencies + updateDependencies({ + workspace: { name: "root", paths: project.paths }, + project, + to, + logger, + options, + }); + + // workspace dependencies + logger.workspaceHeader(); + project.workspaceData.workspaces.forEach((workspace) => { + updateDependencies({ workspace, project, to, logger, options }); + }); + } else if (!options?.dry) { + writeJSONSync(project.paths.packageJson, packageJson, { spaces: 2 }); + } +} + +/** + * Remove bun workspace data + * + * Removing bun workspaces involves: + * 1. Removing the workspaces field from package.json + * 2. Removing the node_modules directory + */ +async function remove(args: RemoveArgs): Promise { + const { project, logger, options } = args; + const hasWorkspaces = project.workspaceData.globs.length > 0; + + logger.mainStep( + getMainStep({ packageManager: "bun", action: "remove", project }) + ); + const packageJson = getPackageJson({ workspaceRoot: project.paths.root }); + + if (hasWorkspaces) { + logger.subStep( + `removing "workspaces" field in ${project.name} root "package.json"` + ); + delete packageJson.workspaces; + } + + logger.subStep( + `removing "packageManager" field in ${project.name} root "package.json"` + ); + delete packageJson.packageManager; + + if (!options?.dry) { + writeJSONSync(project.paths.packageJson, packageJson, { spaces: 2 }); + + // collect all workspace node_modules directories + const allModulesDirs = [ + project.paths.nodeModules, + ...project.workspaceData.workspaces.map((w) => w.paths.nodeModules), + ]; + try { + logger.subStep(`removing "node_modules"`); + await Promise.all( + allModulesDirs.map((dir) => rm(dir, { recursive: true, force: true })) + ); + } catch (err) { + throw new ConvertError("Failed to remove node_modules", { + type: "error_removing_node_modules", + }); + } + } +} + +/** + * Clean is called post install, and is used to clean up any files + * from this package manager that were needed for install, + * but not required after migration + */ +// eslint-disable-next-line @typescript-eslint/require-await -- must match the clean type signature +async function clean(args: CleanArgs): Promise { + const { project, logger, options } = args; + + logger.subStep( + `removing ${path.relative(project.paths.root, project.paths.lockfile)}` + ); + if (!options?.dry) { + rmSync(project.paths.lockfile, { force: true }); + } +} + +/** + * Attempts to convert an existing, non bun lockfile to a bun lockfile + * + * If this is not possible, the non bun lockfile is removed + */ +// eslint-disable-next-line @typescript-eslint/require-await -- must match the convertLock type signature +async function convertLock(args: ConvertArgs): Promise { + const { project, options } = args; + + if (project.packageManager !== "bun") { + // remove the lockfile + if (!options?.dry) { + rmSync(project.paths.lockfile, { force: true }); + } + } +} + +export const bun: ManagerHandler = { + detect, + read, + create, + remove, + clean, + convertLock, +}; diff --git a/packages/turbo-workspaces/src/managers/index.ts b/packages/turbo-workspaces/src/managers/index.ts index 1bd8b8dd93249..f143b1d323678 100644 --- a/packages/turbo-workspaces/src/managers/index.ts +++ b/packages/turbo-workspaces/src/managers/index.ts @@ -1,10 +1,13 @@ -import type { ManagerHandler, PackageManager } from "../types"; +import type { PackageManager } from "@turbo/utils"; +import type { ManagerHandler } from "../types"; import { pnpm } from "./pnpm"; import { npm } from "./npm"; import { yarn } from "./yarn"; +import { bun } from "./bun"; export const MANAGERS: Record = { pnpm, yarn, npm, + bun, }; diff --git a/packages/turbo-workspaces/src/managers/pnpm.ts b/packages/turbo-workspaces/src/managers/pnpm.ts index f8d14e2374738..ef1df18fe2d56 100644 --- a/packages/turbo-workspaces/src/managers/pnpm.ts +++ b/packages/turbo-workspaces/src/managers/pnpm.ts @@ -205,25 +205,33 @@ async function clean(args: CleanArgs): Promise { * If this is not possible, the non pnpm lockfile is removed */ async function convertLock(args: ConvertArgs): Promise { - const { project, logger, options } = args; + const { project, logger, options, to } = args; if (project.packageManager !== "pnpm") { - logger.subStep( - `converting ${path.relative( - project.paths.root, - project.paths.lockfile - )} to pnpm-lock.yaml` - ); - if (!options?.dry && existsSync(project.paths.lockfile)) { - try { - await execa("pnpm", ["import"], { - stdio: "ignore", - cwd: project.paths.root, - }); - } finally { - rmSync(project.paths.lockfile, { force: true }); + // pnpm does not support importing a bun lockfile + if (to.name !== "bun") { + logger.subStep( + `converting ${path.relative( + project.paths.root, + project.paths.lockfile + )} to pnpm-lock.yaml` + ); + + if (!options?.dry && existsSync(project.paths.lockfile)) { + try { + await execa("pnpm", ["import"], { + stdio: "ignore", + cwd: project.paths.root, + }); + } catch (err) { + // do nothing + } } } + + if (!options?.dry) { + rmSync(project.paths.lockfile, { force: true }); + } } } diff --git a/packages/turbo-workspaces/src/types.ts b/packages/turbo-workspaces/src/types.ts index 41149ca6328cb..c5cde2dc90936 100644 --- a/packages/turbo-workspaces/src/types.ts +++ b/packages/turbo-workspaces/src/types.ts @@ -1,7 +1,6 @@ +import type { PackageManager } from "@turbo/utils"; import type { Logger } from "./logger"; -export type PackageManager = "npm" | "pnpm" | "yarn"; - export interface RequestedPackageManagerDetails { name: PackageManager; version?: string; @@ -72,6 +71,7 @@ export interface CleanArgs { export interface ConvertArgs { project: Project; + to: AvailablePackageManagerDetails; logger: Logger; options?: Options; } diff --git a/packages/turbo-workspaces/src/updateDependencies.ts b/packages/turbo-workspaces/src/updateDependencies.ts index 6688bb30ae9bb..4dddbbe32ca6f 100644 --- a/packages/turbo-workspaces/src/updateDependencies.ts +++ b/packages/turbo-workspaces/src/updateDependencies.ts @@ -37,6 +37,10 @@ function updateDependencyList({ return { dependencyList, updated }; } +/** + * Convert workspace dependencies to the format that `to` requires. Only needed when pnpm is involved as + * it requires `workspace:*` and all the rest support `*` + */ export function updateDependencies({ project, workspace, @@ -50,10 +54,10 @@ export function updateDependencies({ logger: Logger; options?: Options; }): void { - // this step isn't required if moving between yarn / npm + // this step isn't required if moving between yarn / npm / bun if ( - ["yarn", "npm"].includes(to.name) && - ["yarn", "npm"].includes(project.packageManager) + ["yarn", "npm", "bun"].includes(to.name) && + ["yarn", "npm", "bun"].includes(project.packageManager) ) { return; } diff --git a/packages/turbo-workspaces/src/utils.ts b/packages/turbo-workspaces/src/utils.ts index 9186753293211..69eb66eaba787 100644 --- a/packages/turbo-workspaces/src/utils.ts +++ b/packages/turbo-workspaces/src/utils.ts @@ -2,13 +2,8 @@ import path from "node:path"; import { readJsonSync, existsSync, readFileSync } from "fs-extra"; import { sync as globSync } from "fast-glob"; import yaml from "js-yaml"; -import type { PackageJson } from "@turbo/utils"; -import type { - PackageManager, - Project, - Workspace, - WorkspaceInfo, -} from "./types"; +import type { PackageJson, PackageManager } from "@turbo/utils"; +import type { Project, Workspace, WorkspaceInfo } from "./types"; import { ConvertError } from "./errors"; // adapted from https://github.com/nodejs/corepack/blob/cae770694e62f15fed33dd8023649d77d96023c1/sources/specUtils.ts#L14 @@ -163,7 +158,7 @@ function expandWorkspaces({ } return workspaceGlobs .flatMap((workspaceGlob) => { - const workspacePackageJsonGlob = `${workspaceGlob}/package.json`; + const workspacePackageJsonGlob = [`${workspaceGlob}/package.json`]; return globSync(workspacePackageJsonGlob, { onlyFiles: true, absolute: true, @@ -206,6 +201,47 @@ function getMainStep({ } ${action === "remove" ? "from" : "to"} ${project.name}`; } +/** + * At the time of writing, bun only support simple globs (can only end in /*) for workspaces. This means we can't convert all projects + * from other package manager workspaces to bun workspaces, we first have to validate that the globs are compatible. + * + * NOTE: It's possible a project could work with bun workspaces, but just not in the way it is currently defined. We will + * not change existing globs to make a project work with bun, we will only convert projects that are already compatible. + * + * This function matches the behavior of bun's glob validation: https://github.com/oven-sh/bun/blob/main/src/install/lockfile.zig#L2889 + */ +function isCompatibleWithBunWorkspace({ + project, +}: { + project: Project; +}): boolean { + const validator = (glob: string) => { + if (glob.includes("*")) { + // no multi level globs + if (glob.includes("**")) { + return false; + } + + // no * in the middle of a path + const withoutLastPathSegment = glob + .split(path.sep) + .slice(0, -1) + .join(path.sep); + if (withoutLastPathSegment.includes("*")) { + return false; + } + } + // no fancy glob patterns + if (["!", "[", "]", "{", "}"].some((char) => glob.includes(char))) { + return false; + } + + return true; + }; + + return project.workspaceData.globs.every(validator); +} + export { getPackageJson, getWorkspacePackageManager, @@ -216,4 +252,5 @@ export { getPnpmWorkspaces, directoryInfo, getMainStep, + isCompatibleWithBunWorkspace, }; From 618f86703a807e253c6a52c28eada31af8585fd4 Mon Sep 17 00:00:00 2001 From: tknickman Date: Thu, 14 Sep 2023 08:59:08 -0400 Subject: [PATCH 2/2] Review updates --- .../turbo-workspaces/__tests__/utils.test.ts | 4 +- packages/turbo-workspaces/src/install.ts | 6 +- packages/turbo-workspaces/src/managers/bun.ts | 55 ++++++++--- packages/turbo-workspaces/src/managers/npm.ts | 51 +++++++--- .../turbo-workspaces/src/managers/pnpm.ts | 96 +++++++++++++------ .../turbo-workspaces/src/managers/yarn.ts | 65 ++++++++++--- packages/turbo-workspaces/src/types.ts | 5 + packages/turbo-workspaces/src/utils.ts | 63 ++++++++++-- 8 files changed, 264 insertions(+), 81 deletions(-) diff --git a/packages/turbo-workspaces/__tests__/utils.test.ts b/packages/turbo-workspaces/__tests__/utils.test.ts index 4c9ec32dbdccd..868f0802c0d8c 100644 --- a/packages/turbo-workspaces/__tests__/utils.test.ts +++ b/packages/turbo-workspaces/__tests__/utils.test.ts @@ -1,5 +1,5 @@ import type { Project } from "../src/types"; -import { isCompatibleWithBunWorkspace } from "../src/utils"; +import { isCompatibleWithBunWorkspaces } from "../src/utils"; describe("utils", () => { describe("isCompatibleWithBunWorkspace", () => { @@ -12,7 +12,7 @@ describe("utils", () => { { globs: ["apps/*", "packages/*/utils/*"], expected: false }, { globs: ["internal-*/*"], expected: false }, ])("should return $result when given %globs", ({ globs, expected }) => { - const result = isCompatibleWithBunWorkspace({ + const result = isCompatibleWithBunWorkspaces({ project: { workspaceData: { globs }, } as Project, diff --git a/packages/turbo-workspaces/src/install.ts b/packages/turbo-workspaces/src/install.ts index 67eb21faabdfa..588b1d0a886ed 100644 --- a/packages/turbo-workspaces/src/install.ts +++ b/packages/turbo-workspaces/src/install.ts @@ -70,13 +70,13 @@ export const PACKAGE_MANAGERS: Record< ], bun: [ { - name: "bun1", + name: "bun", template: "bun", command: "bun", installArgs: ["install"], - version: "1.x", + version: "latest", executable: "bunx", - semver: "<2", + semver: "^1.0.1", default: true, }, ], diff --git a/packages/turbo-workspaces/src/managers/bun.ts b/packages/turbo-workspaces/src/managers/bun.ts index e5289d70cdd3d..69be76b88a996 100644 --- a/packages/turbo-workspaces/src/managers/bun.ts +++ b/packages/turbo-workspaces/src/managers/bun.ts @@ -11,6 +11,7 @@ import type { CleanArgs, Project, ManagerHandler, + Manager, } from "../types"; import { getMainStep, @@ -20,9 +21,15 @@ import { expandWorkspaces, getWorkspacePackageManager, parseWorkspacePackages, - isCompatibleWithBunWorkspace, + isCompatibleWithBunWorkspaces, + removeLockFile, } from "../utils"; +const PACKAGE_MANAGER_DETAILS: Manager = { + name: "bun", + lock: "bun.lockb", +}; + /** * Check if a given project is using bun workspaces * Verify by checking for the existence of: @@ -31,11 +38,13 @@ import { */ // eslint-disable-next-line @typescript-eslint/require-await -- must match the detect type signature async function detect(args: DetectArgs): Promise { - const lockFile = path.join(args.workspaceRoot, "bun.lockb"); + const lockFile = path.join(args.workspaceRoot, PACKAGE_MANAGER_DETAILS.lock); const packageManager = getWorkspacePackageManager({ workspaceRoot: args.workspaceRoot, }); - return existsSync(lockFile) || packageManager === "bun"; + return ( + existsSync(lockFile) || packageManager === PACKAGE_MANAGER_DETAILS.name + ); } /** @@ -57,10 +66,10 @@ async function read(args: ReadArgs): Promise { return { name, description, - packageManager: "bun", + packageManager: PACKAGE_MANAGER_DETAILS.name, paths: expandPaths({ root: args.workspaceRoot, - lockFile: "bun.lockb", + lockFile: PACKAGE_MANAGER_DETAILS.lock, }), workspaceData: { globs: workspaceGlobs, @@ -86,7 +95,7 @@ async function create(args: CreateArgs): Promise { const { project, to, logger, options } = args; const hasWorkspaces = project.workspaceData.globs.length > 0; - if (!isCompatibleWithBunWorkspace({ project })) { + if (!isCompatibleWithBunWorkspaces({ project })) { throw new ConvertError( "Unable to convert project to bun - workspace globs unsupported", { @@ -96,7 +105,11 @@ async function create(args: CreateArgs): Promise { } logger.mainStep( - getMainStep({ packageManager: "bun", action: "create", project }) + getMainStep({ + packageManager: PACKAGE_MANAGER_DETAILS.name, + action: "create", + project, + }) ); const packageJson = getPackageJson({ workspaceRoot: project.paths.root }); logger.rootHeader(); @@ -156,7 +169,11 @@ async function remove(args: RemoveArgs): Promise { const hasWorkspaces = project.workspaceData.globs.length > 0; logger.mainStep( - getMainStep({ packageManager: "bun", action: "remove", project }) + getMainStep({ + packageManager: PACKAGE_MANAGER_DETAILS.name, + action: "remove", + project, + }) ); const packageJson = getPackageJson({ workspaceRoot: project.paths.root }); @@ -219,11 +236,23 @@ async function clean(args: CleanArgs): Promise { async function convertLock(args: ConvertArgs): Promise { const { project, options } = args; - if (project.packageManager !== "bun") { - // remove the lockfile - if (!options?.dry) { - rmSync(project.paths.lockfile, { force: true }); - } + // handle moving lockfile from `packageManager` to npm + switch (project.packageManager) { + case "pnpm": + // can't convert from pnpm to bun - just remove the lock + removeLockFile({ project, options }); + break; + case "bun": + // we're already using bun, so we don't need to convert + break; + case "npm": + // can't convert from npm to bun - just remove the lock + removeLockFile({ project, options }); + break; + case "yarn": + // can't convert from yarn to bun - just remove the lock + removeLockFile({ project, options }); + break; } } diff --git a/packages/turbo-workspaces/src/managers/npm.ts b/packages/turbo-workspaces/src/managers/npm.ts index 78d7d537de7f7..74d3c422ecd26 100644 --- a/packages/turbo-workspaces/src/managers/npm.ts +++ b/packages/turbo-workspaces/src/managers/npm.ts @@ -11,6 +11,7 @@ import type { Project, ConvertArgs, ManagerHandler, + Manager, } from "../types"; import { getMainStep, @@ -20,8 +21,14 @@ import { getWorkspacePackageManager, expandPaths, parseWorkspacePackages, + removeLockFile, } from "../utils"; +const PACKAGE_MANAGER_DETAILS: Manager = { + name: "npm", + lock: "package-lock.json", +}; + /** * Check if a given project is using npm workspaces * Verify by checking for the existence of: @@ -30,11 +37,13 @@ import { */ // eslint-disable-next-line @typescript-eslint/require-await -- must match the detect type signature async function detect(args: DetectArgs): Promise { - const lockFile = path.join(args.workspaceRoot, "package-lock.json"); + const lockFile = path.join(args.workspaceRoot, PACKAGE_MANAGER_DETAILS.lock); const packageManager = getWorkspacePackageManager({ workspaceRoot: args.workspaceRoot, }); - return existsSync(lockFile) || packageManager === "npm"; + return ( + existsSync(lockFile) || packageManager === PACKAGE_MANAGER_DETAILS.name + ); } /** @@ -56,10 +65,10 @@ async function read(args: ReadArgs): Promise { return { name, description, - packageManager: "npm", + packageManager: PACKAGE_MANAGER_DETAILS.name, paths: expandPaths({ root: args.workspaceRoot, - lockFile: "package-lock.json", + lockFile: PACKAGE_MANAGER_DETAILS.lock, }), workspaceData: { globs: workspaceGlobs, @@ -85,7 +94,11 @@ async function create(args: CreateArgs): Promise { const hasWorkspaces = project.workspaceData.globs.length > 0; logger.mainStep( - getMainStep({ packageManager: "npm", action: "create", project }) + getMainStep({ + packageManager: PACKAGE_MANAGER_DETAILS.name, + action: "create", + project, + }) ); const packageJson = getPackageJson({ workspaceRoot: project.paths.root }); logger.rootHeader(); @@ -144,7 +157,11 @@ async function remove(args: RemoveArgs): Promise { const hasWorkspaces = project.workspaceData.globs.length > 0; logger.mainStep( - getMainStep({ packageManager: "npm", action: "remove", project }) + getMainStep({ + packageManager: PACKAGE_MANAGER_DETAILS.name, + action: "remove", + project, + }) ); const packageJson = getPackageJson({ workspaceRoot: project.paths.root }); @@ -207,11 +224,23 @@ async function clean(args: CleanArgs): Promise { async function convertLock(args: ConvertArgs): Promise { const { project, options } = args; - if (project.packageManager !== "npm") { - // remove the lockfile - if (!options?.dry) { - rmSync(project.paths.lockfile, { force: true }); - } + // handle moving lockfile from `packageManager` to npm + switch (project.packageManager) { + case "pnpm": + // can't convert from pnpm to npm - just remove the lock + removeLockFile({ project, options }); + break; + case "bun": + // can't convert from bun to npm - just remove the lock + removeLockFile({ project, options }); + break; + case "npm": + // we're already using npm, so we don't need to convert + break; + case "yarn": + // can't convert from yarn to npm - just remove the lock + removeLockFile({ project, options }); + break; } } diff --git a/packages/turbo-workspaces/src/managers/pnpm.ts b/packages/turbo-workspaces/src/managers/pnpm.ts index ef1df18fe2d56..4f439331558c4 100644 --- a/packages/turbo-workspaces/src/managers/pnpm.ts +++ b/packages/turbo-workspaces/src/managers/pnpm.ts @@ -12,6 +12,7 @@ import type { CleanArgs, Project, ManagerHandler, + Manager, } from "../types"; import { getMainStep, @@ -21,8 +22,15 @@ import { getPnpmWorkspaces, getPackageJson, getWorkspacePackageManager, + removeLockFile, + bunLockToYarnLock, } from "../utils"; +const PACKAGE_MANAGER_DETAILS: Manager = { + name: "pnpm", + lock: "pnpm-lock.yaml", +}; + /** * Check if a given project is using pnpm workspaces * Verify by checking for the existence of: @@ -31,7 +39,7 @@ import { */ // eslint-disable-next-line @typescript-eslint/require-await -- must match the detect type signature async function detect(args: DetectArgs): Promise { - const lockFile = path.join(args.workspaceRoot, "pnpm-lock.yaml"); + const lockFile = path.join(args.workspaceRoot, PACKAGE_MANAGER_DETAILS.lock); const workspaceFile = path.join(args.workspaceRoot, "pnpm-workspace.yaml"); const packageManager = getWorkspacePackageManager({ workspaceRoot: args.workspaceRoot, @@ -39,7 +47,7 @@ async function detect(args: DetectArgs): Promise { return ( existsSync(lockFile) || existsSync(workspaceFile) || - packageManager === "pnpm" + packageManager === PACKAGE_MANAGER_DETAILS.name ); } @@ -58,10 +66,10 @@ async function read(args: ReadArgs): Promise { return { name, description, - packageManager: "pnpm", + packageManager: PACKAGE_MANAGER_DETAILS.name, paths: expandPaths({ root: args.workspaceRoot, - lockFile: "pnpm-lock.yaml", + lockFile: PACKAGE_MANAGER_DETAILS.lock, workspaceConfig: "pnpm-workspace.yaml", }), workspaceData: { @@ -88,7 +96,11 @@ async function create(args: CreateArgs): Promise { const hasWorkspaces = project.workspaceData.globs.length > 0; logger.mainStep( - getMainStep({ action: "create", packageManager: "pnpm", project }) + getMainStep({ + action: "create", + packageManager: PACKAGE_MANAGER_DETAILS.name, + project, + }) ); const packageJson = getPackageJson({ workspaceRoot: project.paths.root }); @@ -144,7 +156,11 @@ async function remove(args: RemoveArgs): Promise { const hasWorkspaces = project.workspaceData.globs.length > 0; logger.mainStep( - getMainStep({ action: "remove", packageManager: "pnpm", project }) + getMainStep({ + action: "remove", + packageManager: PACKAGE_MANAGER_DETAILS.name, + project, + }) ); const packageJson = getPackageJson({ workspaceRoot: project.paths.root }); @@ -205,33 +221,55 @@ async function clean(args: CleanArgs): Promise { * If this is not possible, the non pnpm lockfile is removed */ async function convertLock(args: ConvertArgs): Promise { - const { project, logger, options, to } = args; - - if (project.packageManager !== "pnpm") { - // pnpm does not support importing a bun lockfile - if (to.name !== "bun") { - logger.subStep( - `converting ${path.relative( - project.paths.root, - project.paths.lockfile - )} to pnpm-lock.yaml` - ); + const { project, options, logger } = args; - if (!options?.dry && existsSync(project.paths.lockfile)) { - try { - await execa("pnpm", ["import"], { - stdio: "ignore", - cwd: project.paths.root, - }); - } catch (err) { - // do nothing - } + const logLockConversionStep = (): void => { + logger.subStep( + `converting ${path.relative( + project.paths.root, + project.paths.lockfile + )} to ${PACKAGE_MANAGER_DETAILS.lock}` + ); + }; + + const importLockfile = async (): Promise => { + if (!options?.dry && existsSync(project.paths.lockfile)) { + try { + await execa(PACKAGE_MANAGER_DETAILS.name, ["import"], { + stdio: "ignore", + cwd: project.paths.root, + }); + } catch (err) { + // do nothing + } finally { + removeLockFile({ project, options }); } } + }; - if (!options?.dry) { - rmSync(project.paths.lockfile, { force: true }); - } + // handle moving lockfile from `packageManager` to npm + switch (project.packageManager) { + case "pnpm": + // we're already using pnpm, so we don't need to convert + break; + case "bun": + logLockConversionStep(); + // convert bun -> yarn -> pnpm + await bunLockToYarnLock({ project, options }); + await importLockfile(); + // remove the intermediate yarn lockfile + rmSync(path.join(project.paths.root, "yarn.lock"), { force: true }); + break; + case "npm": + // convert npm -> pnpm + logLockConversionStep(); + await importLockfile(); + break; + case "yarn": + // convert yarn -> pnpm + logLockConversionStep(); + await importLockfile(); + break; } } diff --git a/packages/turbo-workspaces/src/managers/yarn.ts b/packages/turbo-workspaces/src/managers/yarn.ts index 3a1f0c2944f76..3cd7f91616199 100644 --- a/packages/turbo-workspaces/src/managers/yarn.ts +++ b/packages/turbo-workspaces/src/managers/yarn.ts @@ -11,6 +11,7 @@ import type { CleanArgs, Project, ManagerHandler, + Manager, } from "../types"; import { getMainStep, @@ -20,8 +21,15 @@ import { expandWorkspaces, getWorkspacePackageManager, parseWorkspacePackages, + removeLockFile, + bunLockToYarnLock, } from "../utils"; +const PACKAGE_MANAGER_DETAILS: Manager = { + name: "yarn", + lock: "yarn.lock", +}; + /** * Check if a given project is using yarn workspaces * Verify by checking for the existence of: @@ -30,11 +38,13 @@ import { */ // eslint-disable-next-line @typescript-eslint/require-await -- must match the detect type signature async function detect(args: DetectArgs): Promise { - const lockFile = path.join(args.workspaceRoot, "yarn.lock"); + const lockFile = path.join(args.workspaceRoot, PACKAGE_MANAGER_DETAILS.lock); const packageManager = getWorkspacePackageManager({ workspaceRoot: args.workspaceRoot, }); - return existsSync(lockFile) || packageManager === "yarn"; + return ( + existsSync(lockFile) || packageManager === PACKAGE_MANAGER_DETAILS.name + ); } /** @@ -56,10 +66,10 @@ async function read(args: ReadArgs): Promise { return { name, description, - packageManager: "yarn", + packageManager: PACKAGE_MANAGER_DETAILS.name, paths: expandPaths({ root: args.workspaceRoot, - lockFile: "yarn.lock", + lockFile: PACKAGE_MANAGER_DETAILS.lock, }), workspaceData: { globs: workspaceGlobs, @@ -85,7 +95,11 @@ async function create(args: CreateArgs): Promise { const hasWorkspaces = project.workspaceData.globs.length > 0; logger.mainStep( - getMainStep({ packageManager: "yarn", action: "create", project }) + getMainStep({ + packageManager: PACKAGE_MANAGER_DETAILS.name, + action: "create", + project, + }) ); const packageJson = getPackageJson({ workspaceRoot: project.paths.root }); logger.rootHeader(); @@ -144,7 +158,11 @@ async function remove(args: RemoveArgs): Promise { const hasWorkspaces = project.workspaceData.globs.length > 0; logger.mainStep( - getMainStep({ packageManager: "yarn", action: "remove", project }) + getMainStep({ + packageManager: PACKAGE_MANAGER_DETAILS.name, + action: "remove", + project, + }) ); const packageJson = getPackageJson({ workspaceRoot: project.paths.root }); @@ -203,15 +221,36 @@ async function clean(args: CleanArgs): Promise { * * If this is not possible, the non yarn lockfile is removed */ -// eslint-disable-next-line @typescript-eslint/require-await -- must match the convertLock type signature async function convertLock(args: ConvertArgs): Promise { - const { project, options } = args; + const { project, options, logger } = args; - if (project.packageManager !== "yarn") { - // remove the lockfile - if (!options?.dry) { - rmSync(project.paths.lockfile, { force: true }); - } + const logLockConversionStep = (): void => { + logger.subStep( + `converting ${path.relative( + project.paths.root, + project.paths.lockfile + )} to ${PACKAGE_MANAGER_DETAILS.lock}` + ); + }; + + // handle moving lockfile from `packageManager` to yarn + switch (project.packageManager) { + case "pnpm": + // can't convert from pnpm to yarn - just remove the lock + removeLockFile({ project, options }); + break; + case "bun": + // convert from bun lockfile to yarn + logLockConversionStep(); + await bunLockToYarnLock({ project, options }); + break; + case "npm": + // can't convert from npm to yarn - just remove the lock + removeLockFile({ project, options }); + break; + case "yarn": + // we're already using yarn, so we don't need to convert + break; } } diff --git a/packages/turbo-workspaces/src/types.ts b/packages/turbo-workspaces/src/types.ts index c5cde2dc90936..2659c4fb5b1c3 100644 --- a/packages/turbo-workspaces/src/types.ts +++ b/packages/turbo-workspaces/src/types.ts @@ -1,6 +1,11 @@ import type { PackageManager } from "@turbo/utils"; import type { Logger } from "./logger"; +export interface Manager { + name: PackageManager; + lock: string; +} + export interface RequestedPackageManagerDetails { name: PackageManager; version?: string; diff --git a/packages/turbo-workspaces/src/utils.ts b/packages/turbo-workspaces/src/utils.ts index 69eb66eaba787..9a25fe54024b1 100644 --- a/packages/turbo-workspaces/src/utils.ts +++ b/packages/turbo-workspaces/src/utils.ts @@ -1,9 +1,16 @@ import path from "node:path"; -import { readJsonSync, existsSync, readFileSync } from "fs-extra"; +import execa from "execa"; +import { + readJsonSync, + existsSync, + readFileSync, + rmSync, + writeFile, +} from "fs-extra"; import { sync as globSync } from "fast-glob"; import yaml from "js-yaml"; import type { PackageJson, PackageManager } from "@turbo/utils"; -import type { Project, Workspace, WorkspaceInfo } from "./types"; +import type { Project, Workspace, WorkspaceInfo, Options } from "./types"; import { ConvertError } from "./errors"; // adapted from https://github.com/nodejs/corepack/blob/cae770694e62f15fed33dd8023649d77d96023c1/sources/specUtils.ts#L14 @@ -205,12 +212,12 @@ function getMainStep({ * At the time of writing, bun only support simple globs (can only end in /*) for workspaces. This means we can't convert all projects * from other package manager workspaces to bun workspaces, we first have to validate that the globs are compatible. * - * NOTE: It's possible a project could work with bun workspaces, but just not in the way it is currently defined. We will + * NOTE: It's possible a project could work with bun workspaces, but just not in the way its globs are currently defined. We will * not change existing globs to make a project work with bun, we will only convert projects that are already compatible. * - * This function matches the behavior of bun's glob validation: https://github.com/oven-sh/bun/blob/main/src/install/lockfile.zig#L2889 + * This function matches the behavior of bun's glob validation: https://github.com/oven-sh/bun/blob/92e95c86dd100f167fb4cf8da1db202b5211d2c1/src/install/lockfile.zig#L2889 */ -function isCompatibleWithBunWorkspace({ +function isCompatibleWithBunWorkspaces({ project, }: { project: Project; @@ -223,10 +230,7 @@ function isCompatibleWithBunWorkspace({ } // no * in the middle of a path - const withoutLastPathSegment = glob - .split(path.sep) - .slice(0, -1) - .join(path.sep); + const withoutLastPathSegment = glob.split("/").slice(0, -1).join("/"); if (withoutLastPathSegment.includes("*")) { return false; } @@ -242,6 +246,43 @@ function isCompatibleWithBunWorkspace({ return project.workspaceData.globs.every(validator); } +function removeLockFile({ + project, + options, +}: { + project: Project; + options?: Options; +}) { + if (!options?.dry) { + // remove the lockfile + rmSync(project.paths.lockfile, { force: true }); + } +} + +async function bunLockToYarnLock({ + project, + options, +}: { + project: Project; + options?: Options; +}) { + if (!options?.dry && existsSync(project.paths.lockfile)) { + try { + const { stdout } = await execa("bun", ["bun.lockb"], { + stdin: "ignore", + cwd: project.paths.root, + }); + // write the yarn lockfile + await writeFile(path.join(project.paths.root, "yarn.lock"), stdout); + } catch (err) { + // do nothing + } finally { + // remove the old lockfile + rmSync(project.paths.lockfile, { force: true }); + } + } +} + export { getPackageJson, getWorkspacePackageManager, @@ -252,5 +293,7 @@ export { getPnpmWorkspaces, directoryInfo, getMainStep, - isCompatibleWithBunWorkspace, + isCompatibleWithBunWorkspaces, + removeLockFile, + bunLockToYarnLock, };