UNPKG

@deftomat/opinionated

Version:

Opinionated tooling for JavaScript & TypeScript projects.

243 lines (242 loc) 11.1 kB
import chalk from 'chalk'; import { program } from 'commander'; import inquirer from 'inquirer'; import { readFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { registerExitHandlers } from './src/cleanup.js'; import { ensureConfigs } from './src/configs.js'; import { describeContext, isMonorepoPackageContext } from './src/context.js'; import { lint } from './src/eslint.js'; import { format } from './src/format.js'; import { checkNpmAudit, checkNpmLockIntegrity, fixNpmAudit, fixNpmLockDuplicates, usesNpm } from './src/npm.js'; import { preCommit } from './src/preCommit.js'; import { getIncompleteChecks, updateIncompleteChecks } from './src/store.js'; import { containsTypeScript, runTypeCheck } from './src/typeCheck.js'; import { renderOnePackageWarning, step } from './src/utils.js'; import { checkLockDuplicates, checkLockIntegrity, fixLockDuplicates, usesYarn } from './src/yarn.js'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); readFile(`${__dirname}/../../package.json`) .then(body => JSON.parse(body.toString())) .then(({ version, description }) => { const { bold, gray, red, yellow } = chalk; registerExitHandlers(); program.version(version, '-v, --vers', 'output the current version').description(description); program.command('pre-commit').description('Run pre-commit checks.').action(handlePreCommit); program.command('checkup').description('Check up the project.').action(handleCheckup); program .command('ensure-configs') .description('Ensure that all necessary configs are in place.\n\n' + 'In a normal conditions, running this command is not necessary as \neach check ensures that all configs are in place.') .action(handleEnsureConfigs); program.on('command:*', () => { console.error(red('Invalid command: %s\nSee --help for a list of available commands.'), program.args.join(' ')); }); program.parse(process.argv); if (!process.argv.slice(2).length) { program.outputHelp({ error: true }); } async function handlePreCommit() { const context = await prepareContext({ autoStage: true }); await step({ description: 'Running pre-commit checks', run: () => preCommit(context) }); } async function handleCheckup() { const context = await prepareContext({ autoStage: false }); if (isMonorepoPackageContext(context)) renderOnePackageWarning(context); const incompleteChecks = getIncompleteChecks(context); const { requiredChecks, autoFix } = await inquirer.prompt([ { type: 'checkbox', name: 'requiredChecks', message: 'Select checkup operations:', choices: [ usesYarn(context) && { checked: incompleteChecks.size > 0 ? incompleteChecks.has('integrity') : true, name: `${bold('Integrity')} - ensures that dependencies are installed properly`, short: 'Integrity', value: 'integrity' }, usesYarn(context) && { checked: incompleteChecks.size > 0 ? incompleteChecks.has('duplicates') : true, name: `${bold('Dependency duplicates check')} - ensures no unnecessary dependency duplicates`, short: 'Duplicates', value: 'duplicates' }, usesNpm(context) && { checked: incompleteChecks.size > 0 ? incompleteChecks.has('integrity') : true, name: `${bold('Integrity')} - ensures that dependencies are installed properly`, short: 'Integrity', value: 'integrity' }, usesNpm(context) && { checked: incompleteChecks.size > 0 ? incompleteChecks.has('duplicates') : true, name: `${bold('Dependency duplicates check')} - ensures no unnecessary dependency duplicates`, short: 'Duplicates', value: 'duplicates' }, usesNpm(context) && { checked: incompleteChecks.size > 0 ? incompleteChecks.has('audit') : true, name: `${bold('Packages audit')} - ensures all packages are up to date`, short: 'Audit', value: 'audit' }, { checked: incompleteChecks.size > 0 ? incompleteChecks.has('eslint') : true, name: `${bold('Linter')} - runs ESLint`, short: 'Linter', value: 'eslint' }, containsTypeScript(context) && { checked: incompleteChecks.size > 0 ? incompleteChecks.has('typescript') : true, name: `${bold('TypeScript check')} - detects type errors and unused code`, short: 'TypeScript', value: 'typescript' }, { checked: incompleteChecks.size > 0 ? incompleteChecks.has('prettier') : false, name: `${bold('Formatting')} - runs Prettier`, short: 'Formatter', value: 'prettier' } ].filter(Boolean) }, { type: 'confirm', name: 'autoFix', message: 'Do you want to auto-fix any issues if possible?', default: false, when: ({ requiredChecks }) => requiredChecks.includes('eslint') || requiredChecks.includes('duplicates') || requiredChecks.includes('audit') } ]); if ((await context.git.hasChanges()) && (autoFix || requiredChecks.includes('prettier'))) { const { shouldRun } = await inquirer.prompt([ { type: 'confirm', name: 'shouldRun', message: yellow('Selected operations may affect your non-committed files! Do you want to continue?'), default: false } ]); if (!shouldRun) process.exit(); } updateIncompleteChecks(context, requiredChecks); const checks = []; if (usesYarn(context)) { checks.push({ name: 'integrity', enabled: requiredChecks.includes('integrity'), description: 'Checking yarn.lock integrity', run: () => checkLockIntegrity(context) }); checks.push({ name: 'duplicates', enabled: requiredChecks.includes('duplicates') && autoFix, description: 'Removing dependency duplicates', run: () => fixLockDuplicates(context) }); checks.push({ name: 'duplicates', enabled: requiredChecks.includes('duplicates') && !autoFix, description: 'Detecting dependency duplicates', run: () => checkLockDuplicates(context) }); } if (usesNpm(context)) { checks.push({ name: 'integrity', enabled: requiredChecks.includes('integrity'), description: 'Checking package-lock.json integrity', run: () => checkNpmLockIntegrity(context) }); checks.push({ name: 'duplicates', enabled: requiredChecks.includes('duplicates'), description: 'Removing dependency duplicates', run: () => fixNpmLockDuplicates(context) }); checks.push({ name: 'audit', enabled: requiredChecks.includes('audit') && autoFix, description: 'Fixing the npm audit', run: () => fixNpmAudit(context) }); checks.push({ name: 'audit', enabled: requiredChecks.includes('audit') && !autoFix, description: 'Checking the npm audit', run: () => checkNpmAudit(context) }); } checks.push({ name: 'eslint', enabled: requiredChecks.includes('eslint') && autoFix, description: 'Linting & auto-fixing via ESLint', run: () => lint(context, { autoFix: true }) }); checks.push({ name: 'eslint', enabled: requiredChecks.includes('eslint') && !autoFix, description: 'Linting via ESLint', run: () => lint(context, { autoFix: false }) }); checks.push({ name: 'typescript', enabled: requiredChecks.includes('typescript'), description: 'Running TypeScript checks', run: () => runTypeCheck(context) }); checks.push({ name: 'prettier', enabled: requiredChecks.includes('prettier'), description: 'Formatting with Prettier', run: () => format(context) }); const missingChecks = new Set(requiredChecks); for (const check of checks) { if (!check.enabled) continue; check.result = await step({ description: check.description, run: check.run }); missingChecks.delete(check.name); updateIncompleteChecks(context, missingChecks); } } async function handleEnsureConfigs(cmd) { const context = describeContext(process.cwd()); await step({ description: 'Checking necessary configs', run: () => ensureConfigs(context), success: (addedConfigs) => { if (addedConfigs.length > 0) { return `The following configs have been added into project: ${addedConfigs.join(', ')}`; } return 'All configs are in place'; } }); } async function prepareContext({ autoStage }) { try { const context = describeContext(process.cwd()); await context.git.ensureMinimumGitVersion(); if (!(await context.git.isGitRepository())) { throw Error('Failed to run! Project must be the Git repository.'); } const addedConfigs = await ensureConfigs(context, { autoStage }); if (addedConfigs.length > 0) console.info(gray(`[The following configs have been added into project: ${addedConfigs.join(', ')}]`)); return context; } catch (e) { console.error(red(e.message)); throw process.exit(1); } } });