npm-license-checker
Version:
A lightweight and easy-to-use command-line tool for checking and displaying the licenses of npm packages
465 lines (451 loc) • 17.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const child_process_1 = require("child_process");
const fs_1 = require("fs");
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
function getDependenciesFromPackageJson() {
try {
const packageJson = JSON.parse(fs.readFileSync("package.json", "utf-8"));
const deps = [
...Object.keys(packageJson.dependencies || {}),
...Object.keys(packageJson.devDependencies || {}),
...Object.keys(packageJson.peerDependencies || {}),
];
return [...new Set(deps)]; // Remove duplicates
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
console.error("Error reading package.json:", errorMessage);
process.exit(1);
}
}
function parseOptions() {
const args = process.argv.slice(2);
const options = {
input: null,
output: "license-report",
showTree: false,
checkOutdated: false,
detailed: false,
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "-i" || arg === "--input") {
options.input = args[i + 1];
i++;
}
else if (arg === "-o" || arg === "--output") {
options.output = args[i + 1];
i++;
}
else if (arg === "--tree") {
options.showTree = true;
}
else if (arg === "--outdated") {
options.checkOutdated = true;
}
else if (arg === "--detailed" || arg === "-d") {
options.detailed = true;
}
}
return options;
}
const options = parseOptions();
// Get package names either from input file or package.json
const packageNames = options.input
? fs
.readFileSync(options.input, "utf-8")
.split("\n")
.map((pkg) => pkg.trim())
.filter(Boolean) // Remove empty lines
: getDependenciesFromPackageJson();
const licenseInfo = [];
async function getPackageInfo(packageName) {
return new Promise((resolve, reject) => {
(0, child_process_1.exec)(`npm view ${packageName} --json`, { maxBuffer: 1024 * 1024 * 5 }, (error, stdout, stderr) => {
if (error) {
// If package not found, try with @latest
if (error.message.includes("E404")) {
(0, child_process_1.exec)(`npm view ${packageName}@latest --json`, { maxBuffer: 1024 * 1024 * 5 }, (error, stdout) => {
if (error) {
resolve({
name: packageName,
version: "unknown",
license: "Not found",
description: "Package not found in npm registry",
dependencies: {},
});
return;
}
try {
const info = JSON.parse(stdout);
resolve(processPackageInfo(info, packageName));
}
catch (e) {
resolve({
name: packageName,
version: "unknown",
license: "Error parsing",
description: "Error parsing package information",
dependencies: {},
});
}
});
return;
}
reject(error);
return;
}
try {
const info = JSON.parse(stdout);
resolve(processPackageInfo(info, packageName));
}
catch (e) {
resolve({
name: packageName,
version: "unknown",
license: "Error parsing",
description: "Error parsing package information",
dependencies: {},
});
}
});
});
}
function processPackageInfo(info, packageName) {
// Handle repository field which can be string or object
let repository = info.repository;
if (typeof repository === "object" && repository.url) {
// Clean up common repository URL formats
repository = repository.url.replace(/^git\+/, "").replace(/\.git$/, "");
}
// Handle author field which can be string or object
let author = info.author;
if (typeof author === "string") {
// Try to parse author string in format: "Name <email> (url)"
const authorMatch = author.match(/^([^<(]+?)(?:\s*<([^>]+)>)?(?:\s*\(([^)]+)\))?/);
if (authorMatch) {
author = {
name: authorMatch[1].trim(),
email: authorMatch[2]?.trim(),
url: authorMatch[3]?.trim(),
};
}
}
return {
name: info.name || packageName,
version: info.version || "unknown",
license: info.license || "Unknown",
description: info.description,
author: author,
homepage: info.homepage,
repository: repository,
bugs: info.bugs,
dependencies: info.dependencies || {},
devDependencies: info.devDependencies,
peerDependencies: info.peerDependencies,
keywords: info.keywords,
main: info.main,
types: info.types || info.typings,
scripts: info.scripts,
_id: info._id,
_nodeVersion: info._nodeVersion,
_npmVersion: info._npmVersion,
dist: info.dist,
gitHead: info.gitHead,
_npmUser: info._npmUser,
maintainers: info.maintainers,
contributors: info.contributors,
deprecated: info.deprecated,
};
}
async function buildDependencyTree(packageName, depth = 0, seen = new Set()) {
if (seen.has(packageName) || depth > 5) {
// Prevent infinite loops and too deep trees
return "";
}
seen.add(packageName);
try {
const pkgInfo = await getPackageInfo(packageName);
let tree = `${" ".repeat(depth)}- ${pkgInfo.name}@${pkgInfo.version} (${pkgInfo.license})`;
const deps = pkgInfo.dependencies || {};
if (Object.keys(deps).length > 0) {
tree +=
"\n" +
(await Promise.all(Object.entries(deps).map(async ([depName, version]) => {
return await buildDependencyTree(depName, depth + 1, new Set(seen));
})).then((results) => results.join("\n")));
}
return tree;
}
catch (error) {
return `${" ".repeat(depth)}- ${packageName} (Error fetching info)`;
}
}
async function checkLicenses(packageNames) {
const processedPackages = new Set();
for (const packageName of packageNames) {
if (processedPackages.has(packageName))
continue;
try {
const pkgInfo = await getPackageInfo(packageName);
licenseInfo.push({
[pkgInfo.name]: `${pkgInfo.license} (${pkgInfo.version})`,
});
processedPackages.add(packageName);
console.log(`Processed: ${pkgInfo.name}@${pkgInfo.version} (${pkgInfo.license})`);
}
catch (error) {
console.error(`Error processing ${packageName}:`, error.message);
}
}
await generateMarkdownFile(packageNames);
}
async function checkOutdatedPackages(packages) {
const outdatedInfo = {};
try {
// Run npm outdated command
const { stdout } = await new Promise((resolve, reject) => {
(0, child_process_1.exec)("npm outdated --json --long", { maxBuffer: 1024 * 1024 * 5 }, (error, stdout, stderr) => {
if (error && error.code !== 1) {
// npm outdated exits with 1 when there are outdated packages
reject(error);
return;
}
resolve({ stdout, stderr });
});
});
const outdatedData = JSON.parse(stdout || "{}");
// Process each outdated package
for (const [pkg, data] of Object.entries(outdatedData)) {
outdatedInfo[pkg] = {
current: data.current,
latest: data.latest,
wanted: data.wanted,
isOutdated: data.current !== data.latest,
};
}
}
catch (error) {
console.error("Error checking for outdated packages:", error);
}
return outdatedInfo;
}
function formatAuthor(author) {
if (!author)
return "Unknown";
if (typeof author === "string")
return author;
let result = author.name || "Unknown";
if (author.email)
result += ` <${author.email}>`;
if (author.url)
result += ` (${author.url})`;
return result;
}
function formatDependencies(deps) {
if (!deps)
return "None";
return Object.entries(deps)
.map(([name, version]) => `- ${name}: ${version}`)
.join("\n");
}
function generatePackageDetails(pkg) {
const details = [
`### ${pkg.name}@${pkg.version}`,
`**License:** ${pkg.license || "Not specified"}`,
`**Description:** ${pkg.description || "No description"}`,
`**Author:** ${formatAuthor(pkg.author)}`,
`**Homepage:** ${pkg.homepage || "Not specified"}`,
`**Repository:** ${typeof pkg.repository === "string" ? pkg.repository : pkg.repository?.url || "Not specified"}`,
`**Bugs:** ${typeof pkg.bugs === "string" ? pkg.bugs : pkg.bugs?.url || "Not specified"}`,
`**Main File:** ${pkg.main || "Not specified"}`,
`**TypeScript Types:** ${pkg.types || "Not specified"}`,
`**Deprecated:** ${pkg.deprecated || "No"}`,
`**Dependencies:**\n${formatDependencies(pkg.dependencies)}`,
`**Dev Dependencies:**\n${formatDependencies(pkg.devDependencies)}`,
`**Peer Dependencies:**\n${formatDependencies(pkg.peerDependencies)}`,
`**Keywords:** ${pkg.keywords ? pkg.keywords.join(", ") : "None"}`,
`**NPM Version:** ${pkg._npmVersion || "Unknown"}`,
`**Node Version Required:** ${pkg._nodeVersion || "Not specified"}`,
];
return details.join("\n\n");
}
async function generateMarkdownFile(packages) {
const projectLicenses = {};
const packageVersions = {};
const packageDetails = {};
const detailedPackages = [];
// Helper function to find duplicate package versions
function findDuplicatePackages(packages) {
const versionMap = {};
// Extract package names and versions
packages.forEach((pkg) => {
const versionMatch = pkg.match(/(.+)@([^@]+)$/);
if (versionMatch) {
const [, name, version] = versionMatch;
if (!versionMap[name]) {
versionMap[name] = new Set();
}
versionMap[name].add(version);
}
});
// Filter for packages with multiple versions
return Object.entries(versionMap)
.filter(([_, versions]) => versions.size > 1)
.map(([name, versions]) => ({
name,
versions: Array.from(versions),
}));
}
// Process license information and collect detailed package info
for (const info of licenseInfo) {
const packageFullName = Object.keys(info)[0];
const [packageName, version] = packageFullName.split("@");
const license = info[packageFullName];
const pkgInfo = await getPackageInfo(packageFullName);
if (!projectLicenses[license]) {
projectLicenses[license] = [];
}
projectLicenses[license].push(packageFullName);
if (version && version !== "unknown") {
if (!packageVersions[packageName]) {
packageVersions[packageName] = [];
}
if (!packageVersions[packageName].includes(version)) {
packageVersions[packageName].push(version);
}
}
// Store detailed package info if detailed mode is enabled
if (options.detailed) {
detailedPackages.push(pkgInfo);
}
}
const outputDir = path.resolve(options.output);
(0, fs_1.mkdirSync)(outputDir, { recursive: true });
// Check for outdated packages if requested
let outdatedSection = "";
if (options.checkOutdated) {
const outdatedPackages = await checkOutdatedPackages(packages);
const outdatedList = Object.entries(outdatedPackages)
.filter(([_, info]) => info.isOutdated)
.map(([pkg, info]) => `- **${pkg}**: ${info.current} → ${info.latest} (wanted: ${info.wanted})`);
if (outdatedList.length > 0) {
outdatedSection = `
## Outdated Dependencies
The following packages have newer versions available:
${outdatedList.join("\n")}
To update, run: \`npm update\`
`;
}
else {
outdatedSection =
"\n## Dependencies\n\nAll dependencies are up to date! 🎉\n";
}
}
// Generate dependency tree if enabled
let dependencyTreeSection = "";
if (options.showTree) {
const trees = await Promise.all(packages.map((pkg) => buildDependencyTree(pkg)));
dependencyTreeSection = `
## Dependency Tree
\`\`\`
${trees.join("\n\n")}
\`\`\`
`;
}
// Generate duplicate packages section
const duplicateVersions = findDuplicatePackages(packages);
let duplicatesSection = "";
if (duplicateVersions.length > 0) {
duplicatesSection = `
## Potential Version Conflicts
The following packages have multiple versions in use:
${duplicateVersions
.map(({ name, versions }) => `- **${name}**: ${versions.join(", ")}`)
.join("\n")}
`;
}
// Generate detailed packages section if enabled
let detailedSection = "";
if (options.detailed && detailedPackages.length > 0) {
detailedSection = `
## Detailed Package Information
${detailedPackages.map((pkg) => generatePackageDetails(pkg)).join("\n\n---\n\n")}
`;
}
const markdownContent = `# Dependency License Report
## Summary
### License Distribution
${Object.entries(projectLicenses)
.map(([license, pkgs]) => `- **${license}**: ${pkgs.length} packages`)
.join("\n")}
## Packages with Licenses
${packages
.filter((pkg) => licenseInfo.some((info) => Object.keys(info)[0] === pkg))
.map((pkg) => {
const info = licenseInfo.find((info) => Object.keys(info)[0] === pkg);
return `- **${pkg}**: ${info[pkg]}`;
})
.join("\n")}
## Missing License Information
${packages.filter((pkg) => !licenseInfo.some((info) => Object.keys(info)[0] === pkg)).length > 0
? packages
.filter((pkg) => !licenseInfo.some((info) => Object.keys(info)[0] === pkg))
.map((pkg) => `- ${pkg}`)
.join("\n")
: "No packages with missing license information"}
${outdatedSection}
${duplicatesSection}
${dependencyTreeSection}
${detailedSection}
> Generated at: ${new Date().toISOString()}
`;
const outputPath = path.join(outputDir, "license-report.md");
try {
(0, fs_1.writeFileSync)(outputPath, markdownContent);
console.log(`License report generated at ${outputPath}`);
}
catch (error) {
console.error(`Error writing license report: ${error.message}`);
}
}
// Run the main function if this file is executed directly
if (require.main === module) {
checkLicenses(packageNames);
}