Skip to content

Instantly share code, notes, and snippets.

@pyronaur
Created February 7, 2023 16:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pyronaur/8f5b4608344cefc5346ca8ee096982ac to your computer and use it in GitHub Desktop.
Save pyronaur/8f5b4608344cefc5346ca8ee096982ac to your computer and use it in GitHub Desktop.
Monorepo fixer
#!/usr/bin/env zx
// Pass --verbose to see the full output
$.verbose = argv.verbose;
const php = {
is: (file) => file.match(/\.php$/),
fix: async (file) => {
const result = await nothrow($`pnpm run -w php:autofix ${file}`);
// php:autofix outputs the fixes to stderr for some reason.
// Hide it unless --verbose is passed
if (argv.verbose !== true) {
logResult(file, {
stderr: null,
...result
});
} else {
logResult(file, result);
}
},
lint: async (file) => {
const result = await nothrow($`pnpm run -w php:lint ${file}`);
console.log("\n\n");
console.log("🎨 " + chalk.cyan.bold("Linting") + " " + chalk.yellow.bold(file.replace(`${cwd}/`, '')))
const errorPhrases = [
'FOUND',
'ERROR',
]
if (errorPhrases.some(phrase => result.toString().includes(phrase))) {
console.log(chalk.red.bold("Found issues:"));
console.log(result.toString());
console.log("\n");
} else {
console.log(chalk.green.bold(`Looks good!`));
if (argv.verbose) {
console.log(result.toString());
}
}
console.log("\n");
}
}
const js = {
is: (file) => file.match(/\.(js|jsx|cjs|mjs|ts|tsx|svelte)$/),
fix: async (file) => {
const eslintIgnorePath = fs.existsSync(`${cwd}/.eslintignore`) ? `${cwd}/.eslintignore` : `${gitDirectory}/.eslintignore`;
const result = await $`pnpm run -w lint-file ${file} --fix && ${gitDirectory}/tools/js-tools/node_modules/.bin/prettier --ignore-path ${eslintIgnorePath} --write ${file}`;
logResult(file, result);
},
lint: async (file) => {
const eslintIgnorePath = fs.existsSync(`${cwd}/.eslintignore`) ? `${cwd}/.eslintignore` : `${gitDirectory}/.eslintignore`;
const result = await nothrow($`pnpm run -w lint-file ${file} && ${gitDirectory}/tools/js-tools/node_modules/.bin/prettier --ignore-path ${eslintIgnorePath} --check ${file}`);
console.log(chalk.cyan.bold("Linting") + " " + chalk.bold(file.replace(`${cwd}/`, '')))
const errorPhrases = [
'✖',
'error',
'forgot to run prettier',
]
if (errorPhrases.some(phrase => result.toString().includes(phrase))) {
console.log(chalk.red.bold("Found issues:"));
console.log(result.toString());
console.log("\n");
} else {
console.log(chalk.green.bold(`Looks good!`));
if (argv.verbose) {
console.log(result.toString());
}
}
console.log("\n");
}
}
async function getFilesToFix(target, rootPath, scope = 'any') {
target = path.resolve(target);
if (!fs.existsSync(target)) {
console.error(chalk.red.bold(`"${target}" doesn't exist`));
process.exit();
}
const isFixableFile = (file) => php.is(file) || js.is(file);
const isDirectory = fs.statSync(target).isDirectory();
const files = [];
if (isDirectory) {
if (argv.all) {
const allFiles = await globby([`${target}/**/*`], {
onlyFiles: true,
ignoreFiles: [".eslintignore", ".gitignore", ".prettierignore", `${gitDirectory}/.eslintignore`, `${gitDirectory}/.gitignore`, `${gitDirectory}/.prettierignore}`]
});
files.push(...allFiles.filter(isFixableFile));
} else {
const changedFileList = await $`git diff --name-only --diff-filter=ACMRTUXB HEAD;`;
const changedFiles =
changedFileList.toString().split('\n')
.map(f => path.resolve(rootPath, f))
.filter(isFixableFile)
.filter(file => file.startsWith(target))
files.push(...changedFiles);
}
} else {
files.push(target);
}
return {
js: scope == 'any' || scope === 'js' ? files.filter(js.is) : [],
php: scope == 'any' || scope === 'php' ? files.filter(php.is) : [],
}
}
function logResult(file, result) {
if (result.stderr && !result.stdout.toString().includes("No fixable errors were found")) {
console.log(chalk.bold.red(`Error reported fixing "${file.replace(`${cwd}/`, '')}"`));
console.log(result.stdout);
console.log(chalk.dim("--------------------------------"));
console.log(chalk.bold.red(`exitCode "${result.exitCode}" received:\n`));
console.error(result.stderr);
console.log(chalk.dim("--------------------------------"));
} else {
console.log(`${chalk.magenta.bold("Fixed")} ${chalk.yellow.bold(file.replace(`${cwd}/`, ''))}`);
}
}
async function confirm(q, defaultAnswer = "n") {
let yes_no = `(y/N)`;
if (defaultAnswer === "y") {
yes_no = `(Y/n)`;
}
let answer = await question(`${q} ${yes_no} `);
if (!answer) {
answer = defaultAnswer;
}
return "y" === answer;
}
async function selection(options, selectionQuestion) {
let result;
options.forEach((opt, index) => {
console.log(`> ${chalk.bold(index + 1)}: ${opt} `)
})
const selected = await question(selectionQuestion + " (default: 1): \n") || 1;
result = options[selected - 1];
return result;
}
async function reformatEverything(gitDirectory, target) {
const options = [
`Fix all files in "${target}"`,
'Fix all files in the repo',
]
const selected = await selection(options, 'What do you want to do?');
// This is going to be slow.
// Show the verbose output otherwise it feels like the command is stuck
$.verbose = true;
// Fix all files in the repo
if (selected === options[0]) {
console.log(chalk.bold.green("Fixing all files in the repo..."));
cd(gitDirectory);
await $`pnpm run -w php:autofix`;
await $`pnpm run -w reformat-files`;
}
// Fix all files in the current directory
if (selected === options[1]) {
console.log(chalk.bold.green("Fixing all files in the current directory..."));
let eslintIgnorePath = path.resolve(target, '.eslintignore');
if (!fs.existsSync(eslintIgnorePath)) {
eslintIgnorePath = path.resolve(gitDirectory, '.eslintignore');
}
await $`pnpm run -w php:autofix ${target}`;
await $`${gitDirectory}/tools/js-tools/node_modules/.bin/prettier --ignore-path ${eslintIgnorePath} --write ${target}`;
}
if (!selected) {
console.log(chalk.dim(`Couldn't find an option matching what you specified.`));
console.log(chalk.bold.red("Nothing to do."));
process.exit();
}
}
/**
*
* Run
*
*/
/**
* --all
* look for all changed files in the repo
*/
const gitDirectory = (await $`git rev-parse --show-toplevel`).toString().trim();
if (argv.root) {
await cd(gitDirectory);
}
const cwd = (await $`pwd`).toString().trim();
/**
* --js | --php
* only fix specific file types
*/
let scope = 'any';
if (argv.js) {
scope = 'js';
}
if (argv.php) {
scope = 'php';
}
const task = ['lint', 'fix'].includes(argv._[0]) ? argv._.shift() : 'all';
const target = argv._.length > 0 ? argv._.shift() : cwd;
const files = await getFilesToFix(target, gitDirectory, scope);
const count = Object.values(files).flat().length;
if (count > 0) {
if (argv.root) {
console.log(`Found ${count} files in "${chalk.dim(gitDirectory)}"...\n`);
} else if (count > 1) {
console.log(`Found ${count} files in "${target}"...\n`);
} else {
const file = Object.values(files).flat().pop();
console.log(`Found "${chalk.dim(file)}"...\n`)
}
if (task === "all") {
const fix = [
...files.php.map(php.fix),
...files.js.map(js.fix)
]
console.log(chalk.bold.magenta("\n\nFixing..."));
await Promise.all(fix);
const lint = [
...files.php.map(php.lint),
...files.js.map(js.lint)
]
console.log(chalk.bold.cyan("\n\nLinting..."));
await Promise.all(lint);
} else {
const tasks = [
...files.php.map(php[task]),
...files.js.map(js[task])
]
await Promise.all(tasks);
}
} else if (task === 'fix') {
console.log(chalk.green.bold("Nothing to fix"));
if (await confirm("Do you want to reformat all files?")) {
await reformatEverything(gitDirectory, target);
}
} else {
console.log(chalk.yellow("Nothing to lint or fix in", target));
}
console.log(chalk.bold.green("Done!"));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment