package-detector
Version:
A fast and comprehensive Node.js CLI tool to analyze your project's package.json and detect various package-related issues
240 lines (239 loc) • 10.7 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.clearBundlephobiaCache = clearBundlephobiaCache;
exports.detectHeavyPackages = detectHeavyPackages;
exports.getHeavyPackagesInfo = getHeavyPackagesInfo;
exports.isPackageHeavy = isPackageHeavy;
exports.getSizeRecommendations = getSizeRecommendations;
const axios_1 = __importDefault(require("axios"));
const reporter_1 = require("./reporter");
const utils_1 = require("./utils");
// Cache for bundlephobia results to avoid repeated API calls
const bundlephobiaCache = new Map();
/**
* Clear bundlephobia cache
*/
function clearBundlephobiaCache() {
bundlephobiaCache.clear();
}
/**
* Detect heavy packages using Bundlephobia API (optimized with parallel calls)
*/
function detectHeavyPackages() {
return __awaiter(this, void 0, void 0, function* () {
try {
reporter_1.reporter.printInfo('Checking for heavy packages using Bundlephobia...');
// Get all dependencies
const dependencies = (0, utils_1.getAllDependencies)();
const dependencyNames = Object.keys(dependencies);
if (dependencyNames.length === 0) {
reporter_1.reporter.printInfo('No dependencies found in package.json');
return;
}
const heavyPackages = [];
const sizeThresholds = {
small: 50 * 1024, // 50KB
medium: 100 * 1024, // 100KB
large: 500 * 1024 // 500KB
};
// Get existing unused packages to skip them
const existingResults = reporter_1.reporter.getResults();
const unusedPackages = existingResults.filter(r => { var _a; return r.type === 'unused' && (!((_a = r.metadata) === null || _a === void 0 ? void 0 : _a.category) || r.metadata.category !== 'infrastructure'); }).map(r => r.packageName);
// Filter out packages to check
const packagesToCheck = dependencyNames.filter(pkg => !unusedPackages.includes(pkg));
if (packagesToCheck.length === 0) {
reporter_1.reporter.printSuccess('No packages to check for size');
return;
}
// Process packages in parallel batches to avoid overwhelming the API
const batchSize = 3; // Process 3 packages at a time
const batches = [];
for (let i = 0; i < packagesToCheck.length; i += batchSize) {
batches.push(packagesToCheck.slice(i, i + batchSize));
}
for (const batch of batches) {
// Process batch in parallel
const batchPromises = batch.map((packageName) => __awaiter(this, void 0, void 0, function* () {
try {
reporter_1.reporter.printInfo(`Checking size for ${packageName}...`);
const bundleInfo = yield getBundlephobiaInfo(packageName);
if (bundleInfo) {
const gzipSize = bundleInfo.gzip;
let severity = 'low';
let message = '';
if (gzipSize > sizeThresholds.large) {
severity = 'high';
message = `Very large package: ${formatSize(gzipSize)} (gzipped)`;
}
else if (gzipSize > sizeThresholds.medium) {
severity = 'medium';
message = `Large package: ${formatSize(gzipSize)} (gzipped)`;
}
else if (gzipSize > sizeThresholds.small) {
severity = 'low';
message = `Medium package: ${formatSize(gzipSize)} (gzipped)`;
}
if (gzipSize > sizeThresholds.small) {
return {
type: 'heavy',
packageName,
message,
severity,
metadata: {
size: bundleInfo.size,
gzip: bundleInfo.gzip,
version: bundleInfo.version,
description: bundleInfo.description
}
};
}
}
return null;
}
catch (error) {
// Skip packages that can't be checked
console.warn(`Warning: Could not check size for ${packageName}: ${error}`);
return null;
}
}));
// Wait for batch to complete
const batchResults = yield Promise.all(batchPromises);
// Add non-null results
batchResults.forEach(result => {
if (result) {
heavyPackages.push(result);
}
});
// Add a small delay between batches to avoid rate limiting
if (batches.indexOf(batch) < batches.length - 1) {
yield new Promise(resolve => setTimeout(resolve, 300));
}
}
// Add results to reporter
if (heavyPackages.length > 0) {
reporter_1.reporter.addResults(heavyPackages);
reporter_1.reporter.printInfo(`Found ${heavyPackages.length} heavy packages`);
}
else {
reporter_1.reporter.printSuccess('No heavy packages found');
}
}
catch (error) {
reporter_1.reporter.printError(`Failed to detect heavy packages: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
/**
* Get package information from Bundlephobia API (with caching)
*/
function getBundlephobiaInfo(packageName) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
// Check cache first
if (bundlephobiaCache.has(packageName)) {
return bundlephobiaCache.get(packageName);
}
try {
const url = `https://bundlephobia.com/api/size?package=${encodeURIComponent(packageName)}`;
const response = yield axios_1.default.get(url, {
timeout: 5000, // 5 second timeout
headers: {
'User-Agent': 'package-detector/1.0.0'
}
});
if (response.status === 200 && response.data) {
const result = {
size: response.data.size || 0,
gzip: response.data.gzip || 0,
dependencySizes: response.data.dependencySizes || {},
name: response.data.name || packageName,
version: response.data.version || 'unknown',
description: response.data.description
};
// Cache the result
bundlephobiaCache.set(packageName, result);
return result;
}
// Cache null result
bundlephobiaCache.set(packageName, null);
return null;
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
if (((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 404) {
// Package not found on Bundlephobia
bundlephobiaCache.set(packageName, null);
return null;
}
if (((_b = error.response) === null || _b === void 0 ? void 0 : _b.status) === 429) {
// Rate limited
throw new Error('Rate limited by Bundlephobia API');
}
if (error.code === 'ECONNABORTED') {
// Timeout
throw new Error('Request timeout');
}
}
throw error;
}
});
}
/**
* Format file size in human readable format
*/
function formatSize(bytes) {
if (bytes === 0)
return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
/**
* Get detailed heavy package information
*/
function getHeavyPackagesInfo() {
const dependencies = (0, utils_1.getAllDependencies)();
const totalDependencies = Object.keys(dependencies).length;
// This would need to be called after detectHeavyPackages
// For now, return empty structure
return {
totalDependencies,
heavyPackages: [],
sizeBreakdown: { small: 0, medium: 0, large: 0 }
};
}
/**
* Check if a package is considered heavy based on size thresholds
*/
function isPackageHeavy(gzipSize, customThreshold) {
const threshold = customThreshold || 100 * 1024; // Default 100KB
return gzipSize > threshold;
}
/**
* Get package size recommendations
*/
function getSizeRecommendations(gzipSize) {
const recommendations = [];
if (gzipSize > 500 * 1024) {
recommendations.push('Consider using a lighter alternative');
recommendations.push('Check if you need the full package or just specific modules');
}
else if (gzipSize > 100 * 1024) {
recommendations.push('Consider tree-shaking to reduce bundle size');
recommendations.push('Check if you can use dynamic imports for this package');
}
return recommendations;
}