crapifyme
Version:
Ultra-fast developer productivity CLI tools - remove comments, logs, and more
477 lines • 17.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PackageAnalyzer = void 0;
const child_process_1 = require("child_process");
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
const util_1 = require("util");
const types_1 = require("./types");
const execAsync = (0, util_1.promisify)(child_process_1.exec);
class PackageAnalyzer {
constructor(cwd = process.cwd()) {
this.packageManager = null;
this.cwd = cwd;
}
async findProjectRoot() {
let currentDir = path_1.default.resolve(this.cwd);
const root = path_1.default.parse(currentDir).root;
const lockFiles = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'pnpm-lock.yml'];
while (currentDir !== root) {
try {
await promises_1.default.access(path_1.default.join(currentDir, 'package.json'));
for (const lockFile of lockFiles) {
try {
await promises_1.default.access(path_1.default.join(currentDir, lockFile));
return currentDir;
}
catch {
continue;
}
}
}
catch { }
currentDir = path_1.default.dirname(currentDir);
}
try {
await promises_1.default.access(path_1.default.join(root, 'package.json'));
for (const lockFile of lockFiles) {
try {
await promises_1.default.access(path_1.default.join(root, lockFile));
return root;
}
catch {
continue;
}
}
}
catch { }
return null;
}
async detectPackageManager() {
if (this.packageManager)
return this.packageManager;
const projectRoot = await this.findProjectRoot();
if (!projectRoot) {
throw new Error('No package.json found in current directory or any parent directory');
}
this.cwd = projectRoot;
const lockFiles = [
{ file: 'package-lock.json', type: 'npm', auditCmd: 'npm audit' },
{ file: 'yarn.lock', type: 'yarn', auditCmd: 'yarn audit' },
{ file: 'pnpm-lock.yaml', type: 'pnpm', auditCmd: 'pnpm audit' },
{ file: 'pnpm-lock.yml', type: 'pnpm', auditCmd: 'pnpm audit' }
];
for (const { file, type, auditCmd } of lockFiles) {
try {
await promises_1.default.access(path_1.default.join(this.cwd, file));
const version = await this.getPackageManagerVersion(type);
this.packageManager = {
type,
version,
lockFile: file,
auditCommand: auditCmd
};
if (type === 'npm' || type === 'yarn') {
this.packageManager.workspaces = await this.detectWorkspaces();
}
await this.validateProjectContext();
return this.packageManager;
}
catch {
continue;
}
}
throw new Error('No supported package manager detected (npm, yarn, pnpm)');
}
async getPackageManagerVersion(pm) {
try {
const { stdout } = await execAsync(`${pm} --version`, {
cwd: this.cwd,
maxBuffer: 1024 * 1024
});
return stdout.trim();
}
catch {
return 'unknown';
}
}
async detectWorkspaces() {
try {
const pkgJson = await this.readPackageJson();
if (pkgJson.workspaces) {
return Array.isArray(pkgJson.workspaces)
? pkgJson.workspaces
: pkgJson.workspaces.packages || [];
}
}
catch { }
return undefined;
}
async validateProjectContext() {
try {
const pkgJson = await this.readPackageJson();
const isWorkspaceRoot = !!pkgJson.workspaces &&
(!pkgJson.dependencies || Object.keys(pkgJson.dependencies).length === 0);
if (isWorkspaceRoot) {
console.warn('⚠️ Running in workspace root with minimal dependencies.');
console.warn('💡 For better analysis, run from a specific workspace package (e.g., cd packages/website && npx crapifyme deps)');
}
}
catch { }
}
async readPackageJson(filePath) {
const packagePath = filePath || path_1.default.join(this.cwd, 'package.json');
try {
const content = await promises_1.default.readFile(packagePath, 'utf-8');
const packageJson = JSON.parse(content);
return {
name: packageJson.name || 'unnamed-project',
version: packageJson.version || '0.0.0',
description: packageJson.description,
homepage: packageJson.homepage,
repository: packageJson.repository,
keywords: packageJson.keywords,
license: packageJson.license,
...packageJson
};
}
catch (error) {
throw new Error(`Failed to read package.json: ${error.message}`);
}
}
async getInstalledDependencies() {
const pm = await this.detectPackageManager();
const dependencies = new Map();
try {
const pkgJson = await this.readPackageJson();
const depTypes = [
{ deps: pkgJson.dependencies || {}, type: types_1.DependencyType.PRODUCTION },
{ deps: pkgJson.devDependencies || {}, type: types_1.DependencyType.DEVELOPMENT },
{ deps: pkgJson.peerDependencies || {}, type: types_1.DependencyType.PEER },
{ deps: pkgJson.optionalDependencies || {}, type: types_1.DependencyType.OPTIONAL }
];
for (const { deps, type } of depTypes) {
for (const [name, version] of Object.entries(deps)) {
const depInfo = {
name,
currentVersion: version,
isOutdated: false,
isDev: type === types_1.DependencyType.DEVELOPMENT,
isOptional: type === types_1.DependencyType.OPTIONAL,
isPeer: type === types_1.DependencyType.PEER
};
dependencies.set(name, depInfo);
}
}
const outdatedInfo = await this.checkOutdatedDependencies();
for (const [name, info] of outdatedInfo) {
if (dependencies.has(name)) {
const dep = dependencies.get(name);
dep.latestVersion = info.latestVersion;
dep.wantedVersion = info.wantedVersion;
dep.isOutdated = info.isOutdated;
}
}
}
catch (error) {
throw new Error(`Failed to analyze dependencies: ${error.message}`);
}
return dependencies;
}
async checkOutdatedDependencies() {
const pm = await this.detectPackageManager();
const outdated = new Map();
try {
let command;
let parser;
switch (pm.type) {
case 'npm':
command = 'npm outdated --json';
parser = this.parseNpmOutdated.bind(this);
break;
case 'yarn':
command = 'yarn outdated --json';
parser = this.parseYarnOutdated.bind(this);
break;
case 'pnpm':
command = 'pnpm outdated --format json';
parser = this.parsePnpmOutdated.bind(this);
break;
default:
throw new Error(`Unsupported package manager: ${pm.type}`);
}
const { stdout } = await execAsync(command, {
cwd: this.cwd,
timeout: 30000,
maxBuffer: 10 * 1024 * 1024
});
if (stdout.trim()) {
return parser(stdout);
}
}
catch (error) {
if (error.code !== 1) {
console.warn(`Warning: Failed to check outdated dependencies: ${error.message}`);
}
}
return outdated;
}
parseNpmOutdated(output) {
const outdated = new Map();
try {
const data = JSON.parse(output);
for (const [name, info] of Object.entries(data)) {
const pkgInfo = info;
outdated.set(name, {
name,
currentVersion: pkgInfo.current,
latestVersion: pkgInfo.latest,
wantedVersion: pkgInfo.wanted,
isOutdated: true,
isDev: false,
isOptional: false,
isPeer: false
});
}
}
catch (error) {
console.warn(`Warning: Failed to parse npm outdated output: ${error.message}`);
}
return outdated;
}
parseYarnOutdated(output) {
const outdated = new Map();
try {
const lines = output.split('\n').filter(line => line.trim());
for (const line of lines) {
const data = JSON.parse(line);
if (data.type === 'table' && data.data?.body) {
for (const row of data.data.body) {
const [name, current, wanted, latest] = row;
if (name && current !== latest) {
outdated.set(name, {
name,
currentVersion: current,
latestVersion: latest,
wantedVersion: wanted,
isOutdated: true,
isDev: false,
isOptional: false,
isPeer: false
});
}
}
}
}
}
catch (error) {
console.warn(`Warning: Failed to parse yarn outdated output: ${error.message}`);
}
return outdated;
}
parsePnpmOutdated(output) {
const outdated = new Map();
try {
const data = JSON.parse(output);
if (Array.isArray(data)) {
for (const pkg of data) {
if (pkg.current !== pkg.latest) {
outdated.set(pkg.packageName, {
name: pkg.packageName,
currentVersion: pkg.current,
latestVersion: pkg.latest,
wantedVersion: pkg.wanted,
isOutdated: true,
isDev: pkg.dependencyType === 'devDependencies',
isOptional: pkg.dependencyType === 'optionalDependencies',
isPeer: pkg.dependencyType === 'peerDependencies'
});
}
}
}
}
catch (error) {
console.warn(`Warning: Failed to parse pnpm outdated output: ${error.message}`);
}
return outdated;
}
async getDependencyTree() {
const pm = await this.detectPackageManager();
try {
let command;
let parser;
switch (pm.type) {
case 'npm':
command = 'npm ls --json --depth=0';
parser = this.parseNpmTree.bind(this);
break;
case 'yarn':
command = 'yarn list --json --depth=0';
parser = this.parseYarnTree.bind(this);
break;
case 'pnpm':
command = 'pnpm ls --json --depth=0';
parser = this.parsePnpmTree.bind(this);
break;
default:
throw new Error(`Unsupported package manager: ${pm.type}`);
}
const { stdout } = await execAsync(command, {
cwd: this.cwd,
timeout: 30000,
maxBuffer: 5 * 1024 * 1024
});
return parser(stdout);
}
catch (error) {
return this.createBasicTreeFromPackageJson();
}
}
parseNpmTree(output) {
const data = JSON.parse(output);
return this.convertNpmNodeToTreeNode(data);
}
convertNpmNodeToTreeNode(node) {
const treeNode = {
name: node.name || 'root',
version: node.version || '0.0.0',
path: node.path || this.cwd,
dev: node.dev || false,
optional: node.optional || false,
resolved: node.resolved
};
if (node.dependencies) {
treeNode.dependencies = new Map();
for (const [name, depNode] of Object.entries(node.dependencies)) {
treeNode.dependencies.set(name, this.convertNpmNodeToTreeNode(depNode));
}
}
return treeNode;
}
parseYarnTree(output) {
const lines = output.split('\n').filter(line => line.trim());
let rootNode = null;
for (const line of lines) {
try {
const data = JSON.parse(line);
if (data.type === 'tree' && data.data?.trees?.length > 0) {
const tree = data.data.trees[0];
rootNode = {
name: tree.name?.split('@')[0] || 'root',
version: tree.name?.split('@')[1] || '0.0.0',
path: this.cwd,
dev: false,
optional: false,
dependencies: new Map()
};
break;
}
}
catch {
continue;
}
}
return (rootNode || {
name: 'root',
version: '0.0.0',
path: this.cwd,
dev: false,
optional: false
});
}
parsePnpmTree(output) {
const data = JSON.parse(output);
if (Array.isArray(data) && data.length > 0) {
const root = data[0];
return {
name: root.name || 'root',
version: root.version || '0.0.0',
path: root.path || this.cwd,
dev: false,
optional: false,
dependencies: new Map()
};
}
return {
name: 'root',
version: '0.0.0',
path: this.cwd,
dev: false,
optional: false
};
}
async findDuplicateDependencies() {
const tree = await this.getDependencyTree();
const duplicates = new Map();
const versionMap = new Map();
if (!tree)
return duplicates;
this.collectVersions(tree, versionMap);
for (const [name, versions] of versionMap) {
if (versions.size > 1) {
duplicates.set(name, Array.from(versions));
}
}
return duplicates;
}
collectVersions(node, versionMap) {
if (node.name !== 'root') {
if (!versionMap.has(node.name)) {
versionMap.set(node.name, new Set());
}
versionMap.get(node.name).add(node.version);
}
if (node.dependencies) {
for (const [, childNode] of node.dependencies) {
this.collectVersions(childNode, versionMap);
}
}
}
async createBasicTreeFromPackageJson() {
try {
const pkgJson = await this.readPackageJson();
const dependencies = new Map();
const allDeps = {
...pkgJson.dependencies,
...pkgJson.devDependencies,
...pkgJson.peerDependencies,
...pkgJson.optionalDependencies
};
for (const [name, version] of Object.entries(allDeps)) {
dependencies.set(name, {
name,
version: version,
path: this.cwd,
dev: !!pkgJson.devDependencies?.[name],
optional: !!pkgJson.optionalDependencies?.[name]
});
}
return {
name: pkgJson.name || 'root',
version: pkgJson.version || '0.0.0',
path: this.cwd,
dev: false,
optional: false,
dependencies
};
}
catch {
return null;
}
}
formatSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(unitIndex === 0 ? 0 : 1)}${units[unitIndex]}`;
}
}
exports.PackageAnalyzer = PackageAnalyzer;
//# sourceMappingURL=package-analyzer.js.map