license-kit
Version:
Aggregate license notes of OSS libraries used in your Node.js project, analyze & visualize OSS licenses with AI-turbocharged tooling
212 lines (172 loc) • 7.19 kB
text/typescript
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import {
LicenseCategory,
STRONG_COPYLEFT_LICENSES_LOWERCASE,
WEAK_COPYLEFT_LICENSES_LOWERCASE,
analyzeLicenses,
categorizeLicense,
scanDependencies,
} from '@callstack/licenses';
import { bold, green, italic, red, underline, whiteBright, yellow, yellowBright } from 'colorette';
import type { Command } from 'commander';
import { type TableUserConfig, getBorderCharacters, table } from 'table';
import { createScanOptionsFactory } from '../scanOptionsUtils';
import { curryCommonScanOptions } from '../utils/commandUtils';
const tableConfig: TableUserConfig = {
border: getBorderCharacters('norc'),
};
const categoryToEmojiMapping: Record<LicenseCategory, string> = {
[LicenseCategory.PERMISSIVE]: '🔓',
[LicenseCategory.WEAK_COPYLEFT]: '🟡',
[LicenseCategory.STRONG_COPYLEFT]: '🔒',
[LicenseCategory.UNKNOWN]: '❓',
};
function getLicenseColor(license: string): string {
if (license === 'unknown') return yellow(license);
if (WEAK_COPYLEFT_LICENSES_LOWERCASE.has(license)) return yellow(license);
if (STRONG_COPYLEFT_LICENSES_LOWERCASE.has(license)) return red(license);
return license;
}
export default function analyzeCommandSetup(program: Command): Command {
return curryCommonScanOptions(
program
.command('analyze')
.description(
'Scan licenses & report the insights: summary, top license types, optionally unknowns & breakdown of licenses by different features.',
)
.option('--root [path]', 'Path to the root of your project', '.')
.option('--list-unknown', 'List unknown licenses', false)
.option('--show-breakdown', 'Show breakdown of licenses by category and type', false),
).action((options) => {
const repoRootPath = path.resolve(process.cwd(), options.root);
const packageJsonPath = path.join(repoRootPath, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
console.error(`package.json not found at ${packageJsonPath}`);
process.exit(1);
}
const { name = null } = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (name) {
console.log(underline(`💼 Project: ${whiteBright(name)}`));
}
console.log();
const licenses = scanDependencies(packageJsonPath, createScanOptionsFactory(options));
const { byCategory, byLicense, categorizedLicenses, total, description, categoriesPresence } =
analyzeLicenses(licenses);
console.log();
const summaryColor = categoriesPresence.hasAllPermissive
? green
: categoriesPresence.hasAnyUnknown
? yellowBright
: categoriesPresence.hasAnyStrongCopyleft
? red
: yellow;
console.log(`📦 ${bold('Total packages')}: ${whiteBright(total)}`);
console.log(`🔓 ${summaryColor('Graph state summary')}: ${description}`);
const byLicenseEntries = Object.entries(byLicense);
console.log(
`🧮 ${underline('Top 5')} licenses in your project: ${byLicenseEntries
.sort(([, a], [, b]) => b - a)
.slice(0, 5)
.map(([license, count]) => `${whiteBright(license)} (${count})`)
.join(', ')} ${
byLicenseEntries.length > 5
? italic(
yellow(
` + ${byLicenseEntries.length - 5} more (${options.showBreakdown ? 'see below' : 'pass --show-breakdown to view'})`,
),
)
: ''
}`,
);
console.log();
// breakdown of categories checklist
const unknownCount = byCategory[LicenseCategory.UNKNOWN];
const hasUnknown = unknownCount > 0;
const copyleftCount = byCategory[LicenseCategory.STRONG_COPYLEFT];
const hasCopyleft = copyleftCount > 0;
const weakCopyleftCount = byCategory[LicenseCategory.WEAK_COPYLEFT];
const hasWeakCopyleft = weakCopyleftCount > 0;
console.log(
`${categoryToEmojiMapping[LicenseCategory.STRONG_COPYLEFT]} Copyleft licenses: ${(hasCopyleft ? red : green)(
copyleftCount,
)} ${hasCopyleft ? '⚠️' : '✅'}`,
);
console.log(
`${categoryToEmojiMapping[LicenseCategory.WEAK_COPYLEFT]} Weak copyleft licenses: ${(hasWeakCopyleft
? yellow
: green)(weakCopyleftCount)} ${hasWeakCopyleft ? '⚠️' : '✅'}`,
);
console.log(
`${categoryToEmojiMapping[LicenseCategory.UNKNOWN]} Unknown licenses: ${(hasUnknown ? yellow : green)(
unknownCount,
)} ${hasUnknown ? '⚠️' : '✅'}`,
);
console.log(
`${categoryToEmojiMapping[LicenseCategory.PERMISSIVE]} Permissive licenses: ${green(
byCategory[LicenseCategory.PERMISSIVE],
)}`,
);
if (options.listUnknown) {
console.log();
console.log('―'.repeat(process.stdout.columns));
console.log();
console.log(`🔍 ${whiteBright('Unknown licenses')}`);
console.log(
table(
Object.entries(licenses)
.filter(([_packageKey, license]) => categorizeLicense(license.type) === LicenseCategory.UNKNOWN)
.map(([packageKey]) => [packageKey]),
tableConfig,
),
);
}
if (options.showBreakdown) {
console.log();
console.log('―'.repeat(process.stdout.columns));
console.log();
// licenses by category
const byCategoryTable: (string | number)[][] = [['Category', 'Count', 'Percentage']];
Object.entries(byCategory).forEach(([category, count]) => {
byCategoryTable.push([category, count, Number(((count / total) * 100).toFixed(2))]);
});
console.log(`🗂️ Licenses by ${whiteBright('category')}`);
console.log(table(byCategoryTable, tableConfig));
console.log();
// licenses by type
const byLicenseTable: (string | number)[][] = [['License', 'Count', 'Percentage']];
byLicenseEntries
.sort(([, a], [, b]) => b - a)
.forEach(([license, count]) => {
byLicenseTable.push([
license === 'unknown' ? yellow(license) : getLicenseColor(license),
count,
Number(((count / total) * 100).toFixed(2)),
]);
});
console.log(`🏷️ Licenses by ${whiteBright('type')}`);
console.log(table(byLicenseTable, tableConfig));
console.log();
const nonFullyPermissiveObj = [
['License', 'Category'],
...Object.entries(categorizedLicenses)
.filter(([_license, category]) => category !== LicenseCategory.PERMISSIVE)
.map(([license, category]) => [license, `${category} ${categoryToEmojiMapping[category]}`]),
];
// non-permissive licenses
if (nonFullyPermissiveObj.length > 0) {
console.log(`⚠️ ${whiteBright('Non-fully-permissive')} licenses`);
console.log(table(nonFullyPermissiveObj, tableConfig));
} else {
console.log(`✅ ${whiteBright('All licenses are fully-permissive')}`);
}
}
console.log();
console.log(
italic(
'Remember that all data presented by the tool require manual verification. The presented information may be inaccurate or incomplete.',
),
);
});
}