import type { CAC } from 'cac'; import type { Result } from 'publint'; import { basename, dirname, join } from 'node:path'; import { colors, consola, ensureFile, findMonorepoRoot, generatorContentHash, getPackages, outputJSON, readJSON, UNICODE, } from '@vben/node-utils'; import { publint } from 'publint'; import { formatMessage } from 'publint/utils'; const CACHE_FILE = join( 'node_modules', '.cache', 'publint', '.pkglintcache.json', ); interface PubLintCommandOptions { /** * Only errors are checked, no program exit is performed */ check?: boolean; } /** * Get files that require lint * @param files */ async function getLintFiles(files: string[] = []) { const lintFiles: string[] = []; if (files?.length > 0) { return files.filter((file) => basename(file) === 'package.json'); } const { packages } = await getPackages(); for (const { dir } of packages) { lintFiles.push(join(dir, 'package.json')); } return lintFiles; } function getCacheFile() { const root = findMonorepoRoot(); return join(root, CACHE_FILE); } async function readCache(cacheFile: string) { try { await ensureFile(cacheFile); return await readJSON(cacheFile); } catch { return {}; } } async function runPublint(files: string[], { check }: PubLintCommandOptions) { const lintFiles = await getLintFiles(files); const cacheFile = getCacheFile(); const cacheData = await readCache(cacheFile); const cache: Record = cacheData; const results = await Promise.all( lintFiles.map(async (file) => { try { const pkgJson = await readJSON(file); if (pkgJson.private) { return null; } Reflect.deleteProperty(pkgJson, 'dependencies'); Reflect.deleteProperty(pkgJson, 'devDependencies'); Reflect.deleteProperty(pkgJson, 'peerDependencies'); const content = JSON.stringify(pkgJson); const hash = generatorContentHash(content); const publintResult: Result = cache?.[file]?.hash === hash ? (cache?.[file]?.result ?? []) : await publint({ level: 'suggestion', pkgDir: dirname(file), strict: true, }); cache[file] = { hash, result: publintResult, }; return { pkgJson, pkgPath: file, publintResult }; } catch { return null; } }), ); await outputJSON(cacheFile, cache); printResult(results, check); } function printResult( results: Array<{ pkgJson: Record; pkgPath: string; publintResult: Result; } | null>, check?: boolean, ) { let errorCount = 0; let warningCount = 0; let suggestionsCount = 0; for (const result of results) { if (!result) { continue; } const { pkgJson, pkgPath, publintResult } = result; const messages = publintResult?.messages ?? []; if (messages?.length < 1) { continue; } consola.log(''); consola.log(pkgPath); for (const message of messages) { switch (message.type) { case 'error': { errorCount++; break; } case 'warning': { warningCount++; break; } case 'suggestion': { suggestionsCount++; break; } // No default } const ruleUrl = `https://publint.dev/rules#${message.code.toLocaleLowerCase()}`; consola.log( ` ${formatMessage(message, pkgJson)}${colors.dim(` ${ruleUrl}`)}`, ); } } const totalCount = warningCount + errorCount + suggestionsCount; if (totalCount > 0) { consola.error( colors.red( `${UNICODE.FAILURE} ${totalCount} problem (${errorCount} errors, ${warningCount} warnings, ${suggestionsCount} suggestions)`, ), ); !check && process.exit(1); } else { consola.log(colors.green(`${UNICODE.SUCCESS} No problem`)); } } function definePubLintCommand(cac: CAC) { cac .command('publint [...files]') .usage('Check if the monorepo package conforms to the publint standard.') .option('--check', 'Only errors are checked, no program exit is performed.') .action(runPublint); } export { definePubLintCommand };