@bfra.me/eslint-config
Version:
Shared ESLint configuration for bfra.me
177 lines (150 loc) • 4.85 kB
text/typescript
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
}
}