UNPKG

lint-to-the-future

Version:

A modern way to progressively update your code to the best practices

396 lines (332 loc) 11.5 kB
#!/usr/bin/env node import { join, dirname } from 'path'; import { writeFileSync, readFileSync } from 'fs'; import { createRequire } from 'module'; import { fileURLToPath } from 'url'; import { Command } from 'commander'; import { inspect } from 'util'; import process from 'node:process'; import difference from 'set.prototype.difference'; import { cp, mkdir } from 'node:fs/promises'; const __dirname = dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); const program = new Command(); async function getPreviousResults(previousResultsPath) { let previousResults = {}; if (previousResultsPath) { if (previousResultsPath.match(/((http(s?)):\/\/)/)) { const response = await fetch(previousResultsPath); if (response.ok) { previousResults = await response.json(); } else { console.warn( `Error ${response.status} when downloading previous results. Previous results ignored`, ); } } else { try { let file = readFileSync(previousResultsPath); previousResults = JSON.parse(file); } catch { console.warn( `Error ${previousResultsPath} could not be found. Previous results ignored`, ); } } } return previousResults; } async function list(lttfPlugins, previousResultsPath) { let pluginResults = {}; for (let plugin of lttfPlugins) { pluginResults[plugin.name] = await plugin.import.list(); } let previousResults = await getPreviousResults(previousResultsPath); // Squash old entries for (let date in previousResults) { for (let plugin in previousResults[date]) { for (let rule in previousResults[date][plugin]) { if (Array.isArray(previousResults[date][plugin][rule])) { previousResults[date][plugin][rule] = previousResults[date][plugin][rule].length; } } } } let today = new Date(); let isoDate = today.toISOString().split('T')[0]; previousResults[isoDate] = pluginResults; return previousResults; } async function output({ lttfPlugins, outputPath, rootUrl, previousResultsPath, outputFolders, }) { if (!output) { console.error('You must provide an output directory to `output` with -o'); } const ouputResult = await list(lttfPlugins, previousResultsPath); await cp(join(__dirname, 'dist'), outputPath, { recursive: true }); if (rootUrl) { let index = readFileSync(join(outputPath, 'index.html'), 'utf8'); writeFileSync( join(outputPath, 'index.html'), index .replace(`rootURL: '/',`, `rootURL: '/${rootUrl}/',`) .replace(/"\/assets\//g, `"/${rootUrl}/assets/`), ); } if (outputFolders) { let index = readFileSync(join(outputPath, 'index.html'), 'utf8'); const ruleNames = new Set(); for (let [, plugins] of Object.entries(ouputResult)) { for (let plugin in plugins) { for (let rule in plugins[plugin]) { ruleNames.add(`${plugin}:${rule}`); } } } for (let ruleName of ruleNames) { let ruleFolder = join(outputPath, 'rule', ruleName); await mkdir(ruleFolder, { recursive: true }); writeFileSync(join(ruleFolder, 'index.html'), index); } } writeFileSync(join(outputPath, 'data.json'), JSON.stringify(ouputResult)); } async function diff(lttfPlugins, previousResultsPath) { let currentResult = {}; for (let plugin of lttfPlugins) { currentResult[plugin.name] = await plugin.import.list(); } let previousResults = await getPreviousResults(previousResultsPath); if (Object.keys(previousResults).length === 0) { console.error('No previous results found, cannot generate diff'); } let lastDate = Object.keys(previousResults).sort().reverse()[0]; let lastResult = previousResults[lastDate]; let allPlugins = new Set([ ...Object.keys(currentResult), ...Object.keys(lastResult), ]); let added = {}; let removed = {}; for (let plugin of allPlugins) { let allPluginRules = new Set([ ...Object.keys(currentResult[plugin] || {}), ...Object.keys(lastResult[plugin] || {}), ]); for (let rule of allPluginRules) { const currentFiles = new Set(currentResult[plugin]?.[rule]); const lastFiles = new Set(lastResult[plugin]?.[rule]); const addedFiles = difference(currentFiles, lastFiles); if (addedFiles.size) { added[plugin] = added[plugin] ?? {}; added[plugin][rule] = [...addedFiles]; } const removedFiles = difference(lastFiles, currentFiles); if (removedFiles.size) { removed[plugin] = removed[plugin] ?? {}; removed[plugin][rule] = [...removedFiles]; } } } return { added, removed }; } async function getLttfPlugins() { let currentPackageJSON = require(join(process.cwd(), 'package.json')); let lttfPluginsNames = [ ...Object.keys(currentPackageJSON.devDependencies || {}), ...Object.keys(currentPackageJSON.dependencies || {}), ].filter( (dep) => dep.startsWith('lint-to-the-future-') || dep.includes('/lint-to-the-future-'), ); let lttfPlugins = []; // note: this is different from `currentPackageJSON` because it's creating a require for `process.cwd()` instead of using `require` // to import the ``join(process.cwd(), 'package.json')` const pluginRequire = createRequire(join(process.cwd(), 'package.json')); for (const name of lttfPluginsNames) { let mod = pluginRequire(name); lttfPlugins.push({ import: mod, name, }); } return lttfPlugins; } function getFirstObjectFromObjectKeys(object) { let key = Object.keys(object)[0]; return object[key]; } const packageJson = require(join(__dirname, 'package.json')); program .name('lint-to-the-future') .description( 'A modern way to progressively update your code to the best practices using lint rules', ) .version(packageJson.version); program .command('output') .description( 'Generates a dashboard webpage to help track which files have file-based ignore directives in them', ) .requiredOption('-o, --output <path>', 'Output path for dashboard webpage') .option( '--rootUrl <path|url>', 'Required if the dashboard is not hosted on your servers rootUrl', ) .option( '--previous-results <path|url>', 'This should be a path or URL to the previous data.json file that was generated when this command was last run', ) .option('--output-folders', 'Creates a folder structure for each rule page') .action( async ({ output: outputPath, rootUrl, previousResults, outputFolders }) => { let lttfPlugins = await getLttfPlugins(); output({ lttfPlugins, outputPath, rootUrl, previousResultsPath: previousResults, outputFolders, }); }, ); program .command('list') .description( 'prints the current files with file-based lint ignore directives', ) .option('-o, --output <path>', 'output path for lttf list') .option('--stdout', 'print lttf list to console') .option( '--previous-results <path|url>', 'This should be a path or URL to the previous data.json file that was generated when this command was last run', ) .action(async ({ output: outputPath, stdout, previousResults }) => { if (!outputPath && !stdout) { console.error( 'You must provide an output path to `list` with -o or pass --stdout', ); } let lttfPlugins = await getLttfPlugins(); const listResult = await list(lttfPlugins, previousResults); if (stdout) { console.log(inspect(listResult, { depth: 5 })); } if (outputPath) { writeFileSync(outputPath, JSON.stringify(listResult)); } }); program .command('ignore') .description('Add file-based ignores to any file that is currently erroring') .option('-p, --plugin <string>', 'only run ignore on one plugin') .option('-f, --filter <string>', 'only apply to the filtered files') .action(async (options) => { function executePlugin(plugin) { // this makes sure that even when if (plugin.import.capabilities?.includes('filter-ignore')) { return plugin.import.ignoreAll({ filter: options.filter }); } else { if (options.filter) { program.error( `Plugin ${plugin.name} does not support passing '--filter' to the ignore command. Please update or contact the plugin developers`, ); return; } plugin.import.ignoreAll(); } } let lttfPlugins = await getLttfPlugins(); if (options.plugin) { let plugin = lttfPlugins.find((p) => p.name === options.plugin); if (!plugin) { program.error( `Could not find plugin with specified name ${options.plugin}. Available plugins are: ${lttfPlugins.map((p) => p.name).join(', ')}`, ); return; } await executePlugin(plugin); } else { for (let plugin of lttfPlugins) { await executePlugin(plugin); } } }); program .command('remove') .description('Remove file-based ignores in every file for a specified rule') .argument('<rule-name>', 'Name of lint rule to remove file-based ignores') .option('-f, --filter <string>', 'only apply to the filtered files') .action(async (lintRuleName, options) => { let lttfPlugins = await getLttfPlugins(); let listResult = getFirstObjectFromObjectKeys(await list(lttfPlugins)); let pluginsWithRuleName = []; for (const pluginKey in listResult) { if (listResult[pluginKey][lintRuleName]) { let plugin = lttfPlugins.find((plugin) => plugin.name === pluginKey); pluginsWithRuleName.push(plugin); } } if (!pluginsWithRuleName.length) { console.error( `No file-based ignores could be found for the lint rule '${lintRuleName}'`, ); return; } async function removeIgnore(plugin, lintRuleName) { if (plugin.import.remove) { await plugin.import.remove({ name: lintRuleName, filter: options.filter, }); } else { console.error( `The 'remove' command is not supported by the plugin ${plugin.name}. Please update or contact the plugin developers`, ); } } for (let plugin of pluginsWithRuleName) { await removeIgnore(plugin, lintRuleName); } }); program .command('diff') .description( 'prints changes in file-based ignores compared to previous results', ) .option('-o, --output <path>', 'output path for lttf diff') .option('--stdout', 'print lttf diff to console') .option( '--previous-results <path|url>', 'Path or URL to the previous data.json file that was generated when the `output` command was last run', ) .action(async ({ output: outputPath, stdout, previousResults }) => { if (!outputPath && !stdout) { program.error( 'You must provide an output path to `diff` with -o or pass --stdout', ); return; } if (!previousResults) { program.error( 'You must provide previous results to `diff` using `--previous-results <path|url>`', ); return; } const lttfPlugins = await getLttfPlugins(); const diffResult = await diff(lttfPlugins, previousResults); if (stdout) { console.log(JSON.stringify(diffResult, null, ' ')); } if (outputPath) { writeFileSync(outputPath, JSON.stringify(diffResult)); } }); program.parse();