From c1dcc239faaaccb75ab99aa53db8fa3b7b34e8f9 Mon Sep 17 00:00:00 2001 From: Robert Field Date: Sat, 23 Sep 2023 21:11:45 +0100 Subject: [PATCH] feat: detect users package manager --- .../src/commands/generate/d2c/d2c-command.tsx | 20 ++-- .../src/commands/generate/d2c/d2c.types.ts | 2 +- .../commands/ui/generate/d2c-generated.tsx | 14 ++- .../src/lib/detect-package-manager.ts | 103 ++++++++++++++++++ 4 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 packages/composable-cli/src/lib/detect-package-manager.ts diff --git a/packages/composable-cli/src/commands/generate/d2c/d2c-command.tsx b/packages/composable-cli/src/commands/generate/d2c/d2c-command.tsx index ea4dded1..7372ce6e 100644 --- a/packages/composable-cli/src/commands/generate/d2c/d2c-command.tsx +++ b/packages/composable-cli/src/commands/generate/d2c/d2c-command.tsx @@ -45,6 +45,7 @@ import { createActiveStoreMiddleware, createAuthenticationCheckerMiddleware, } from "../generate-command" +import { detect } from "../../../lib/detect-package-manager" export function createD2CCommand( ctx: CommandContext @@ -63,8 +64,7 @@ export function createD2CCommand( }) .option("pkg-manager", { describe: "node package manager to use", - choices: ["npm", "yarn", "pnpm"] as const, - default: "npm" as const, + choices: ["npm", "yarn", "pnpm", "bun"] as const, }) .help() .parserConfiguration({ @@ -167,8 +167,12 @@ export function createD2CCommandHandler( return async function generateCommandHandler(args) { const colors = ansiColors.create() - const { cliOptions, schematicOptions, _, name, pkgManager } = - parseArgs(args) + const detectedPkgManager = await detect() + + const { cliOptions, schematicOptions, _, name, pkgManager } = parseArgs( + args, + detectedPkgManager + ) /** Create the DevKit Logger used through the CLI. */ const logger = createConsoleLogger( @@ -415,6 +419,7 @@ export function createD2CCommandHandler( skipGit, skipInstall, skipConfig, + packageManager: pkgManager, ...gatheredOptions, }, allowPrivate: allowPrivate, @@ -494,12 +499,13 @@ interface Options { schematicOptions: Record cliOptions: Partial, boolean | null>> name: string | null - pkgManager: "npm" | "yarn" | "pnpm" + pkgManager: "npm" | "yarn" | "pnpm" | "bun" } /** Parse the command line. */ function parseArgs( - args: yargs.ArgumentsCamelCase + args: yargs.ArgumentsCamelCase, + detectedPkgManager?: "npm" | "yarn" | "pnpm" | "bun" ): Options { const { _, $0, name = null, ...options } = args @@ -532,7 +538,7 @@ function parseArgs( schematicOptions, cliOptions, name, - pkgManager: args["pkg-manager"], + pkgManager: args["pkg-manager"] ?? detectedPkgManager ?? "npm", } } diff --git a/packages/composable-cli/src/commands/generate/d2c/d2c.types.ts b/packages/composable-cli/src/commands/generate/d2c/d2c.types.ts index 1748bfbe..23b0d622 100644 --- a/packages/composable-cli/src/commands/generate/d2c/d2c.types.ts +++ b/packages/composable-cli/src/commands/generate/d2c/d2c.types.ts @@ -9,5 +9,5 @@ export type D2CCommandError = { export type D2CCommandArguments = { name?: string - "pkg-manager": "npm" | "yarn" | "pnpm" + "pkg-manager"?: "npm" | "yarn" | "pnpm" | "bun" } & GenerateCommandArguments diff --git a/packages/composable-cli/src/commands/ui/generate/d2c-generated.tsx b/packages/composable-cli/src/commands/ui/generate/d2c-generated.tsx index d423009b..52008cf9 100644 --- a/packages/composable-cli/src/commands/ui/generate/d2c-generated.tsx +++ b/packages/composable-cli/src/commands/ui/generate/d2c-generated.tsx @@ -7,7 +7,7 @@ export function D2CGenerated({ skipInstall, }: { name: string - nodePkgManager: "yarn" | "npm" | "pnpm" + nodePkgManager: "yarn" | "npm" | "pnpm" | "bun" skipInstall: boolean }) { return ( @@ -35,7 +35,7 @@ function constructSteps({ skipInstall, }: { name: string - nodePkgManager: "yarn" | "npm" | "pnpm" + nodePkgManager: "yarn" | "npm" | "pnpm" | "bun" skipInstall: boolean }) { return [ @@ -49,7 +49,7 @@ function constructSteps({ ] } -function resolveStartCommand(nodePkgManager: "yarn" | "npm" | "pnpm") { +function resolveStartCommand(nodePkgManager: "yarn" | "npm" | "pnpm" | "bun") { switch (nodePkgManager) { case "yarn": return "yarn run dev" @@ -57,10 +57,14 @@ function resolveStartCommand(nodePkgManager: "yarn" | "npm" | "pnpm") { return "npm run dev" case "pnpm": return "pnpm run dev" + case "bun": + return "bun run dev" } } -function resolveInstallCommand(nodePkgManager: "yarn" | "npm" | "pnpm") { +function resolveInstallCommand( + nodePkgManager: "yarn" | "npm" | "pnpm" | "bun" +) { switch (nodePkgManager) { case "yarn": return "yarn install" @@ -68,5 +72,7 @@ function resolveInstallCommand(nodePkgManager: "yarn" | "npm" | "pnpm") { return "npm install" case "pnpm": return "pnpm install" + case "bun": + return "bun install" } } diff --git a/packages/composable-cli/src/lib/detect-package-manager.ts b/packages/composable-cli/src/lib/detect-package-manager.ts new file mode 100644 index 00000000..a878e42e --- /dev/null +++ b/packages/composable-cli/src/lib/detect-package-manager.ts @@ -0,0 +1,103 @@ +import { promises as fs } from "fs" +import { resolve } from "path" +import { execa } from "execa" + +export type PM = "npm" | "yarn" | "pnpm" | "bun" + +/** + * Check if a path exists + */ +async function pathExists(p: string) { + try { + await fs.access(p) + return true + } catch { + return false + } +} + +const cache = new Map() + +/** + * Check if a global pm is available + */ +function hasGlobalInstallation(pm: PM): Promise { + const key = `has_global_${pm}` + if (cache.has(key)) { + return Promise.resolve(cache.get(key)) + } + + return execa(pm, ["--version"]) + .then((res) => { + return /^\d+.\d+.\d+$/.test(res.stdout) + }) + .then((value) => { + cache.set(key, value) + return value + }) + .catch(() => false) +} + +function getTypeofLockFile(cwd = "."): Promise { + const key = `lockfile_${cwd}` + if (cache.has(key)) { + return Promise.resolve(cache.get(key)) + } + + return Promise.all([ + pathExists(resolve(cwd, "yarn.lock")), + pathExists(resolve(cwd, "package-lock.json")), + pathExists(resolve(cwd, "pnpm-lock.yaml")), + pathExists(resolve(cwd, "bun.lockb")), + ]).then(([isYarn, isNpm, isPnpm, isBun]) => { + let value: PM | null = null + + if (isYarn) { + value = "yarn" + } else if (isPnpm) { + value = "pnpm" + } else if (isBun) { + value = "bun" + } else if (isNpm) { + value = "npm" + } + + cache.set(key, value) + return value + }) +} + +const detect = async ({ + cwd, + includeGlobalBun, +}: { cwd?: string; includeGlobalBun?: boolean } = {}) => { + const type = await getTypeofLockFile(cwd) + if (type) { + return type + } + const [hasYarn, hasPnpm, hasBun] = await Promise.all([ + hasGlobalInstallation("yarn"), + hasGlobalInstallation("pnpm"), + includeGlobalBun && hasGlobalInstallation("bun"), + ]) + if (hasYarn) { + return "yarn" + } + if (hasPnpm) { + return "pnpm" + } + if (hasBun) { + return "bun" + } + return "npm" +} + +export { detect } + +export function getNpmVersion(pm: PM) { + return execa(pm || "npm", ["--version"]).then((res) => res.stdout) +} + +export function clearCache() { + return cache.clear() +}