UNPKG

knip-guard

Version:

Baseline / no-new-issues guard for Knip

264 lines 9.66 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.runKnipJson = runKnipJson; exports.loadKnipReportFromFile = loadKnipReportFromFile; exports.extractIssueKeys = extractIssueKeys; exports.loadBaseline = loadBaseline; exports.saveBaseline = saveBaseline; exports.diffBaseline = diffBaseline; const promises_1 = require("node:fs/promises"); const node_path_1 = __importDefault(require("node:path")); const node_child_process_1 = require("node:child_process"); const node_util_1 = require("node:util"); const exec = (0, node_util_1.promisify)(node_child_process_1.exec); /** * Run knip with JSON reporter and parse the output * Executes the given command with `--reporter json` flag and returns the parsed report * @param command - Full command to run knip (default: "npx knip") * @returns Parsed Knip JSON report * @throws Error if command fails or output is invalid JSON * @example * const report = await runKnipJson('npm run knip'); */ async function runKnipJson(command = 'npx knip') { const fullCommand = `${command} --reporter json`; const { stdout } = await exec(fullCommand, { maxBuffer: 20 * 1024 * 1024 }); return JSON.parse(stdout); } /** * Load a Knip JSON report from a file * @param reportPath - Path to the Knip JSON report file * @returns Parsed Knip JSON report * @throws Error if file cannot be read or JSON is invalid * @example * const report = await loadKnipReportFromFile('./knip-report.json'); */ async function loadKnipReportFromFile(reportPath) { const abs = node_path_1.default.resolve(reportPath); const content = await (0, promises_1.readFile)(abs, 'utf8'); return JSON.parse(content); } /** * Extract the name from a Knip issue entry * Handles both structured entries with a `name` property and fallback to JSON string * @param entry - The issue entry to extract a name from * @returns The name as a string * @internal */ function getEntryName(entry) { if (entry && typeof entry === 'object' && 'name' in entry && typeof entry.name === 'string') { return entry.name; } return JSON.stringify(entry); } /** * Add deterministic keys for simple issue fields (arrays of entries with names) * Creates keys in format: `fieldName::filePath::entryName` * @param keys - Set to accumulate the generated keys * @param issue - The Knip issue object * @param file - The file path for the issue * @param fields - List of field names to process * @internal */ function addSimpleFieldKeys(keys, issue, file, fields) { for (const field of fields) { const arr = issue[field]; if (!Array.isArray(arr)) continue; for (const item of arr) { const name = getEntryName(item); keys.add(`${String(field)}::${file}::${name}`); } } } /** * Add deterministic keys for enum member issues * Creates keys in format: `enumMembers::filePath::enumName.memberName` * @param keys - Set to accumulate the generated keys * @param issue - The Knip issue object * @param file - The file path for the issue * @internal */ function addEnumMemberKeys(keys, issue, file) { const enumMembers = issue.enumMembers; if (!enumMembers || typeof enumMembers !== 'object') return; for (const enumName of Object.keys(enumMembers)) { const members = enumMembers[enumName]; if (!Array.isArray(members)) continue; for (const member of members) { const memberName = getEntryName(member); keys.add(`enumMembers::${file}::${enumName}.${memberName}`); } } } /** * Add deterministic keys for class member issues * Creates keys in format: `classMembers::filePath::className.memberName` * @param keys - Set to accumulate the generated keys * @param issue - The Knip issue object * @param file - The file path for the issue * @internal */ function addClassMemberKeys(keys, issue, file) { const classMembers = issue.classMembers; if (!classMembers || typeof classMembers !== 'object') return; for (const className of Object.keys(classMembers)) { const members = classMembers[className]; if (!Array.isArray(members)) continue; for (const member of members) { const memberName = getEntryName(member); keys.add(`classMembers::${file}::${className}.${memberName}`); } } } /** * Add deterministic keys for duplicate export issues * Creates keys in format: `duplicates::filePath::exportName` * @param keys - Set to accumulate the generated keys * @param issue - The Knip issue object * @param file - The file path for the issue * @internal */ function addDuplicateKeys(keys, issue, file) { if (!Array.isArray(issue.duplicates)) return; for (const name of issue.duplicates) { if (typeof name !== 'string') continue; keys.add(`duplicates::${file}::${name}`); } } /** * Extract deterministic issue keys from a Knip report * Keys exclude line/column/position information, making them stable across formatting changes * This allows baseline comparisons to work correctly even when code is reformatted * @param report - The Knip JSON report to extract keys from * @returns Set of deterministic issue keys * @example * const report = await runKnipJson(); * const keys = extractIssueKeys(report); * // keys contains: "files::src/unused.ts", "exports::src/foo.ts::unusedFn" */ function extractIssueKeys(report) { const keys = new Set(); // Unused files if (Array.isArray(report.files)) { for (const file of report.files) { if (typeof file === 'string') { keys.add(`files::${file}`); } } } if (!Array.isArray(report.issues)) { return keys; } const simpleFields = [ 'dependencies', 'devDependencies', 'optionalPeerDependencies', 'unlisted', 'unresolved', 'binaries', 'exports', 'nsExports', 'types', 'nsTypes' ]; for (const issue of report.issues) { const file = typeof issue.file === 'string' ? issue.file : '<unknown-file>'; addSimpleFieldKeys(keys, issue, file, simpleFields); addEnumMemberKeys(keys, issue, file); addClassMemberKeys(keys, issue, file); addDuplicateKeys(keys, issue, file); } return keys; } /** * Load baseline data from disk * Returns null if the baseline file does not exist * @param baselinePath - Path to the baseline JSON file * @returns Baseline data or null if file not found * @throws Error if file exists but cannot be read or parsed * @example * const baseline = await loadBaseline('.knip-baseline.json'); */ async function loadBaseline(baselinePath) { try { const abs = node_path_1.default.resolve(baselinePath); const content = await (0, promises_1.readFile)(abs, 'utf8'); return JSON.parse(content); } catch (err) { if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) { return null; } throw err; } } /** * Save baseline data to disk * Creates a new baseline or updates an existing one with metadata timestamps * Issues are automatically sorted for deterministic output * @param baselinePath - Path where the baseline JSON file should be saved * @param issueKeys - Set of deterministic issue keys to include in baseline * @param existing - Existing baseline data (to preserve createdAt timestamp) * @param sourceCommand - Optional command used to generate the baseline * @returns The saved baseline data * @throws Error if file cannot be written * @example * const keys = extractIssueKeys(report); * const baseline = await saveBaseline('.knip-baseline.json', keys, null, 'npm run knip'); */ async function saveBaseline(baselinePath, issueKeys, existing, sourceCommand) { const abs = node_path_1.default.resolve(baselinePath); const now = new Date().toISOString(); const sortedIssues = Array.from(issueKeys).sort(); const data = { createdAt: existing?.createdAt ?? now, updatedAt: now, issues: sortedIssues, sourceCommand: sourceCommand ?? existing?.sourceCommand }; await (0, promises_1.writeFile)(abs, JSON.stringify(data, null, 2) + '\n', 'utf8'); return data; } /** * Compare current issues against a baseline * Identifies new issues that weren't in the baseline and resolved issues that no longer exist * @param baseline - The baseline data to compare against (null is treated as empty) * @param currentIssueKeys - Set of current deterministic issue keys * @returns Object containing arrays of new and resolved issues, both sorted * @example * const baseline = await loadBaseline('.knip-baseline.json'); * const current = extractIssueKeys(report); * const { newIssues, resolvedIssues } = diffBaseline(baseline, current); */ function diffBaseline(baseline, currentIssueKeys) { const baseSet = new Set(baseline?.issues ?? []); const newIssues = []; const resolvedIssues = []; for (const key of currentIssueKeys) { if (!baseSet.has(key)) { newIssues.push(key); } } for (const key of baseSet) { if (!currentIssueKeys.has(key)) { resolvedIssues.push(key); } } newIssues.sort(); resolvedIssues.sort(); return { newIssues, resolvedIssues }; } //# sourceMappingURL=index.js.map