UNPKG

@bfra.me/eslint-config

Version:
177 lines (150 loc) 4.85 kB
import {spawnSync} from 'node:child_process' import fs from 'node:fs' import path from 'node:path' import {AGENTS, LOCKS, resolveCommand, type AgentName} from 'package-manager-detector' export type InstallResult = {success: true} | {success: false; output: string} | null interface PackageJson { packageManager?: string devEngines?: { packageManager?: { name?: string } } [key: string]: unknown } interface ParsedModule { moduleName: string version?: string } interface InstallOptions { cwd: string dev: boolean } const installedModules = new Set<string>() export function tryInstall(module: string, targetFile: string): InstallResult { if (installedModules.has(module)) { return null } const {moduleName, version} = parseModule(module) const packageId = version === undefined ? moduleName : `${moduleName}@^${version}` try { installPackageSync(packageId, { cwd: path.dirname(targetFile), dev: true, }) installedModules.add(module) return {success: true} } catch (error) { installedModules.add(module) const errorMessage = error instanceof Error ? error.message : String(error) return {output: errorMessage, success: false} } } export function getPackageInstallCommand(module: string, targetFile: string): string | null { try { const {args, command} = resolvePackageInstallCommand(module, path.dirname(targetFile)) return `${command} ${args.join(' ')}` } catch { return null } } function parseModule(name: string): ParsedModule { const parts = name.split(/@/u) const hasVersion = parts.length > 1 && parts.slice(0, -1).join('@').length > 0 if (hasVersion) { return { moduleName: parts.slice(0, -1).join('@'), version: parts.at(-1), } } return {moduleName: name} } function resolvePackageInstallCommand( module: string, cwd: string, ): {command: string; args: string[]} { const agent = detectPackageManagerSync(cwd) if (agent == null) { throw new Error(`Unable to detect package manager for cwd: ${cwd}`) } const args: string[] = [] if (agent === 'pnpm' && fs.existsSync(path.resolve(cwd, 'pnpm-workspace.yaml'))) { args.unshift( '-w', // Prevent pnpm from removing installed development deps when NODE_ENV is production '--prod=false', ) } const command = resolveCommand(agent, 'add', [...args, '-D', module]) if (command == null) { throw new Error(`Unable to resolve command for package manager: ${agent}`) } return command } function installPackageSync(packageId: string, options: InstallOptions): string { const {args, command} = resolvePackageInstallCommand(packageId, options.cwd) const result = spawnSync(command, args.filter(Boolean), { cwd: options.cwd, maxBuffer: Infinity, stdio: 'inherit', windowsHide: true, }) if (result.error != null || result.status !== 0) { const errorMessage = result.error?.message ?? `Package installation failed with status ${result.status}` throw new Error(errorMessage) } return result.output?.toString() ?? '' } function isAgentName(name: string): name is AgentName { return AGENTS.includes(name as AgentName) } function getPackageManagerFromPackageJson(cwd: string): AgentName | null { try { const packageJsonPath = path.join(cwd, 'package.json') if (!fs.existsSync(packageJsonPath)) { return null } const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as PackageJson if (typeof packageJson.packageManager === 'string') { const [name] = packageJson.packageManager.split('@') return name != null && isAgentName(name) ? name : null } const devEngines = packageJson.devEngines if (devEngines != null && typeof devEngines === 'object' && 'packageManager' in devEngines) { const pkgManager = devEngines.packageManager if ( pkgManager != null && typeof pkgManager === 'object' && 'name' in pkgManager && typeof pkgManager.name === 'string' ) { return isAgentName(pkgManager.name) ? pkgManager.name : null } } return null } catch { return null } } function detectPackageManagerSync(cwd: string): AgentName | null { try { let directory = path.resolve(cwd) const {root} = path.parse(directory) while (directory && directory !== root) { for (const [lockfile, agent] of Object.entries(LOCKS)) { if (fs.existsSync(path.resolve(directory, lockfile))) { return getPackageManagerFromPackageJson(directory) ?? agent } } const pkgManager = getPackageManagerFromPackageJson(directory) if (pkgManager != null) { return pkgManager } directory = path.dirname(directory) } return null } catch { return null } }