lint-to-the-future
Version:
A modern way to progressively update your code to the best practices
396 lines (332 loc) • 11.5 kB
JavaScript
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();