Skip to content

Instantly share code, notes, and snippets.

@btoo
Last active May 9, 2020 15:13
Show Gist options
  • Save btoo/e7b5bee4a73efc6c0e10e4297738c0df to your computer and use it in GitHub Desktop.
Save btoo/e7b5bee4a73efc6c0e10e4297738c0df to your computer and use it in GitHub Desktop.
husky pre-commit hook to `eslint --fix` and `prettier --write` files staged in git
*dist*
*.json
yarn.lock
*.proto
*.md
Makefile
.eslintignore
/** @see {@tutorial https://blog.totominc.io/linting-vue-typescript-eslint-prettier/} */
module.exports = {
root: true,
env: {
node: true,
},
extends: [
// 'plugin:prettier/recommended', // add prettier-eslint plugin which will uses the `.prettierrc.js` config
'plugin:import/typescript',
// prettier configs need to be last - https://github.com/prettier/eslint-config-prettier#installation
'prettier',
'prettier/@typescript-eslint',
'prettier/babel',
'prettier/standard',
],
rules: {
// you can put some custom rules here
'no-nested-ternary': 'off',
'no-unused-expressions': [
'error',
{ allowShortCircuit: true, allowTernary: true, allowTaggedTemplates: true },
],
'object-curly-spacing': ['error', 'always'],
'import/no-cycle': 'off',
radix: 'off',
'no-console': 'off',
'no-unsafe-finally': 'off',
'consistent-return': 'off',
semi: 'error',
'space-infix-ops': 'error',
'no-multi-spaces': 'error',
'no-return-assign': 'off',
'no-unused-expressions': 'off',
/** @see {@link https://github.com/typescript-eslint/typescript-eslint/issues/600#issuecomment-499979248} */
'spaced-comment': ['error', 'always', { markers: ['/'] }],
'import/prefer-default-export': 'off',
'no-async-promise-executor': 'off',
camelcase: 'off',
'no-underscore-dangle': 'off',
},
parserOptions: {
parser: '@typescript-eslint/parser', // the typescript-parser for eslint, instead of tslint
sourceType: 'module', // allow the use of imports statements
ecmaVersion: 2018, // allow the parsing of modern ecmascript
},
settings: {
'import/resolver': {
/** @see {@tutorial https://www.npmjs.com/package/eslint-import-resolver-typescript#configuration} */
typescript: {
/** always try to resolve types under `<root>@types` directory even it doesn't contain any source code, like `@types/unist` */
alwaysTryTypes: true,
},
/**
* @see {@tutorial https://www.npmjs.com/package/eslint-import-resolver-alias}
* @see {@link import('./build/webpack.config.js').resolve.alias}
*/
alias: {
map: [
['@', './src'],
['@test', require('path').resolve(__dirname, '../test')],
],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
},
};
/**
* @see {@tutorial https://github.com/typicode/husky}
* _Requires Node >= 10 and Git >= 2.13.0._
*/
module.exports = {
hooks: {
/**
* @see {@tutorial https://stackoverflow.com/a/15656652/3942699}
*
* ___only works with files staged in git___
* because doing on the entire repo is overkill atm
*
* if linting succeeds, proceed to committing the staged files.
* if linting fails, prevent the commit from happening and fix the auto-fixable errors
*/
'pre-commit': "npx ts-node -O '{\"module\":\"commonjs\"}' lint",
},
};
module.exports = {
singleQuote: true,
printWidth: 100,
};
import * as execa from 'execa';
const parse = (c: string | string[]) => {
let command = '', args = [];
if (Array.isArray(c)) ([command, ...args] = c);
/** @see {@link https://stackoverflow.com/a/14912552/3942699} */
else ([command, ...args] = c.match(/\S+/g) || [command]);
return { command, args };
};
type Command = Parameters<typeof parse>[0];
const read = async (c: Command) => {
const { command, args } = parse(c);
return (await execa(command, args)).stdout;
};
const run = (c: Command, options?: import('child_process').SpawnOptions) => {
const { command, args } = parse(c);
return execa(command, args, { ...options, stdio: 'inherit' });
};
/**
* performs linting (eslint) and formatting (prettier) on __staged files only__
*
* (probably as a pre-commit hook @see .huskyrc.js)
*/
(async () => {
const { default: chalk } = await import('chalk');
const stagedFiles = (await read('git diff --diff-filter=d --cached --name-only'))
.split('\n')
.filter(Boolean);
if (stagedFiles.length) {
console.log(chalk.blueBright('\nLinting staged files with ESLint before committing...'));
/** eslint only works with js and ts for now */
const stagedFilesForEslint = stagedFiles.filter((f) => f.match(/\.(js|jsx|ts|tsx|json)$/));
const stagedFilesForEslintStr = stagedFilesForEslint.join(' ');
/**
* a reusable function for formatting with Prettier.
* this function should be called after either:
* - linting succeeds or
* - linting fails and then lint --fix is attempted
*/
const formatStagedFilesWithPrettier = async () => {
try {
console.log(
chalk.blueBright('\nFormatting staged files with Prettier before committing...\n')
);
const stagedFilesStr = stagedFiles.join(' ');
await read(`npx prettier --list-different ${stagedFilesStr}`);
// nothing to format
} catch (resultOfCheckingToSeeIfAnythingNeedsToBeFormatted) {
const filesThatNeedToBeFormatted = (resultOfCheckingToSeeIfAnythingNeedsToBeFormatted.stdout as string)
.split('\n')
.filter(Boolean);
if (filesThatNeedToBeFormatted.length) {
try {
await read(`npx prettier --write ${filesThatNeedToBeFormatted.join(' ')}`);
} finally {
const { default: path } = await import('path');
console.log(chalk.redBright.bold('The following files needed formatting:'));
const cwd = process.cwd();
filesThatNeedToBeFormatted.forEach((f) =>
console.log(chalk.underline(path.join(cwd, f)))
);
console.log();
process.exit(1);
}
}
}
};
try {
stagedFilesForEslint.length && (await run(`npx eslint ${stagedFilesForEslintStr}`));
await formatStagedFilesWithPrettier();
} catch (lintingErr) {
// there are linting problems, so:
// 1. try to fix the linting problems with eslint --fix
// 2. try to format the code with prettier --write
// 3. prevent the commit from happening
console.log(chalk.blueBright('Attempting to `--fix` problems...'));
try {
stagedFilesForEslint.length && (await run(`npx eslint --fix ${stagedFilesForEslintStr}`));
await formatStagedFilesWithPrettier();
} finally {
process.exit(1);
}
}
} else {
console.log(chalk.blueBright('\nThere are no files staged in git to lint/format\n'));
}
})();
{
"scripts": {
"postinstall": "node ./node_modules/husky/lib/installer/bin install"
},
"dependencies": {
"chalk": "^3.0.0",
},
"devDependencies": {
"@types/node": "^12.7.2",
"@typescript-eslint/eslint-plugin": "^2.25.0",
"@typescript-eslint/parser": "^2.25.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.1",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-typescript": "^2.0.0",
"eslint-plugin-html": "^4.0.2",
"eslint-plugin-prettier": "^3.1.2",
"execa": "^2.0.3",
"husky": "^4.2.3",
"prettier": "^2.0.2",
"ts-node": "^8.8.1"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment