UNPKG

@swell/cli

Version:

Swell's command line interface/utility

189 lines (188 loc) 7.78 kB
import * as fs from 'node:fs/promises'; import * as path from 'node:path'; /** Lock file to package manager mapping (priority order: bun > pnpm > yarn > npm) */ const LOCK_FILES = [ ['bun.lockb', 'bun'], ['bun.lock', 'bun'], ['pnpm-lock.yaml', 'pnpm'], ['yarn.lock', 'yarn'], ['package-lock.json', 'npm'], ]; /** Command templates for each package manager */ const COMMANDS = { npm: { install: 'npm install', run: (script) => `npm run ${script}`, workspaceExec: (workspace, command) => `npm exec --workspace=${workspace} -- ${command}`, workspaceRun: (workspace, script) => `npm run ${script} --workspace=${workspace}`, exec: (command) => `npx ${command}`, }, yarn: { install: 'yarn install', run: (script) => `yarn run ${script}`, workspaceExec: (workspace, command) => `yarn workspace ${workspace} ${command}`, workspaceRun: (workspace, script) => `yarn workspace ${workspace} run ${script}`, exec: (command) => `yarn dlx ${command}`, }, pnpm: { install: 'pnpm install', run: (script) => `pnpm run ${script}`, workspaceExec: (workspace, command) => `pnpm --filter ${workspace} exec ${command}`, workspaceRun: (workspace, script) => `pnpm --filter ${workspace} run ${script}`, exec: (command) => `pnpm dlx ${command}`, }, bun: { install: 'bun install', run: (script) => `bun run ${script}`, // Use --cwd for executing binaries in a workspace (--filter is for scripts only) workspaceExec: (workspace, command) => `bun --cwd ${workspace} x ${command}`, workspaceRun: (workspace, script) => `bun run --filter ${workspace} ${script}`, exec: (command) => `bunx ${command}`, }, }; /** * Find lock file in directory or parent directories * @returns Lock file name and path, or null if not found */ export async function findLockFile(directory) { let currentDir = path.resolve(directory); const { root } = path.parse(currentDir); while (currentDir !== root) { for (const [lockFile, pm] of LOCK_FILES) { const lockPath = path.join(currentDir, lockFile); // eslint-disable-next-line no-await-in-loop const exists = await fs.access(lockPath, fs.constants.R_OK).then(() => true, (_error) => false); if (exists) { return { name: lockFile, packageManager: pm, path: lockPath }; } } currentDir = path.dirname(currentDir); } return null; } /** * Detect package manager from lock file in directory tree * @returns Detected package manager, defaults to 'npm' if no lock file found */ export async function detectPackageManager(directory) { const lockFile = await findLockFile(directory); return lockFile?.packageManager ?? 'npm'; } /** * Get command templates for a package manager */ export function getPackageManagerCommands(pm) { return COMMANDS[pm]; } /** * Detect package manager and get its commands */ export async function getPackageManager(directory) { const lockFile = await findLockFile(directory); const name = lockFile?.packageManager ?? 'npm'; return { commands: COMMANDS[name], lockFile: lockFile ? { name: lockFile.name, path: lockFile.path } : null, name, }; } /** * Transform an npm command to the equivalent for the detected package manager * Handles common patterns: * - 'npm run X' -> 'yarn run X' / 'pnpm run X' / 'bun run X' * - 'npm run X --workspace=Y' -> 'yarn workspace Y run X' / 'pnpm --filter Y run X' / 'bun run --filter Y X' * - 'npm exec --workspace=X -- Y' -> equivalent workspace command * - 'npx X' -> 'yarn dlx X' / 'pnpm dlx X' / 'bunx X' */ export function transformCommand(command, pm) { if (pm === 'npm') { return command; } const commands = COMMANDS[pm]; // Transform 'npm exec --workspace=X -- Y' const workspaceMatch = command.match(/^npm exec --workspace=(\S+) -- (.+)$/); if (workspaceMatch) { return commands.workspaceExec(workspaceMatch[1], workspaceMatch[2]); } // Transform 'npm run X --workspace=Y' const workspaceRunMatch = command.match(/^npm run (\S+) --workspace=(\S+)$/); if (workspaceRunMatch) { return commands.workspaceRun(workspaceRunMatch[2], workspaceRunMatch[1]); } // Transform 'npm run X' (with optional flags) const runMatch = command.match(/^npm run (\S.*)$/); if (runMatch) { return commands.run(runMatch[1]); } // Transform 'npx X' to 'bunx X' for bun (performance benefit from Bun runtime) // For other package managers, keep npx as-is (universally available via Node.js) if (pm === 'bun') { const npxMatch = command.match(/^npx (.+)$/); if (npxMatch) { return `bunx ${npxMatch[1]}`; } } // Warn if command looks like npm but wasn't transformed if (command.startsWith('npm ')) { console.warn(`Warning: Command "${command}" was not transformed for ${pm}.`); } return command; } /** * Transform npm create commands to the equivalent for another package manager. * Handles 'npm create X@version -- args' -> 'pm create X@version args' * Also transforms --use-npm to --use-{pm} for CLIs that support it (e.g., Next.js) * For yarn, removes @latest suffix since yarn classic (v1) may not handle it properly. * * @example * transformCreateCommand('npm create cloudflare@latest -- frontend --framework=hono', 'bun') * // Returns: 'bun create cloudflare@latest frontend --framework=hono' * * @example * transformCreateCommand('npm create cloudflare@latest -- frontend --framework=next --use-npm', 'pnpm') * // Returns: 'pnpm create cloudflare@latest frontend --framework=next --use-pnpm' * * @example * transformCreateCommand('npm create cloudflare@latest -- frontend --framework=astro', 'yarn') * // Returns: 'yarn create cloudflare -- frontend --framework=astro' */ export function transformCreateCommand(command, pm) { if (pm === 'npm') { return command; } // Match 'npm create X@version -- args' or 'npm create X -- args' const createMatch = command.match(/^npm create (\S+) -- (.+)$/); if (createMatch) { let [, packageWithVersion, args] = createMatch; // Replace --use-npm with --use-{pm} for Next.js and similar CLIs const transformedArgs = args.replace(/--use-npm\b/, `--use-${pm}`); // For yarn classic (v1), remove @latest since yarn create fetches latest by default // and may not properly handle version specifiers. Also add -- separator for args. if (pm === 'yarn') { packageWithVersion = packageWithVersion.replace(/@latest$/, ''); return `yarn create ${packageWithVersion} -- ${transformedArgs}`; } return `${pm} create ${packageWithVersion} ${transformedArgs}`; } return command; } /** * Get spawn arguments for executing a package binary. * Returns the program and any prefix args needed before the command args. * Useful for node's spawn() which needs program and args separately. * * Uses npx for most package managers (universally available via Node.js). * Uses bunx for bun projects (performance benefit from Bun runtime). * * @example * const { program, prefixArgs } = getExecSpawnArgs('npm'); * spawn(program, [...prefixArgs, 'wrangler', 'dev', '--port=3000']); */ export function getExecSpawnArgs(pm) { // Use bunx for bun (performance benefit), npx for everything else // npx is universally available via Node.js and avoids yarn dlx v1/v2 issues if (pm === 'bun') { return { prefixArgs: [], program: 'bunx' }; } return { prefixArgs: [], program: 'npx' }; }