Last active
July 8, 2021 17:15
-
-
Save n8jadams/931f2241a193f7ea1227205cf20ef9f1 to your computer and use it in GitHub Desktop.
Run eslint and prettier against your staged changes. Great when used as a precommit hook. (Assumes it's placed in top level of repo)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const path = require('path') | |
const { execSync, spawnSync } = require('child_process') | |
const prettier = require('prettier') | |
const fs = require('fs') | |
const micromatch = require('micromatch') | |
const lockfile = path.resolve(__dirname, 'linter-script-lockfile') | |
const eslintFileTypes = ['js', 'jsx', 'ts', 'tsx'] | |
const prettierExtParserMap = { | |
json: 'json', | |
js: 'babel', | |
jsx: 'babel', | |
ts: 'typescript', | |
tsx: 'typescript', | |
css: 'css', | |
} | |
const prettierFileTypes = Object.keys(prettierExtParserMap) | |
function readGlobsFromFile(filename) { | |
if (!fs.existsSync(filename)) { | |
return [] | |
} | |
return fs | |
.readFileSync(filename, 'utf-8') | |
.split('\n') | |
.map((f) => f.trim()) | |
.filter(Boolean) | |
} | |
function deleteLockfile() { | |
fs.unlinkSync(lockfile) | |
} | |
function killFunction() { | |
console.log('Halting the lint/format script. Cleaning up...') | |
deleteLockfile() | |
process.exit(0) | |
} | |
process.on('SIGINT', killFunction) | |
process.on('SIGTERM', killFunction) | |
;(function main() { | |
// Ensure this script isn't being run already | |
if (fs.existsSync(lockfile)) { | |
console.error( | |
'ERROR! Cannot run linter/formatter script if it is already running elsewhere' | |
) | |
process.exit(1) | |
} | |
fs.writeFileSync(lockfile, '') | |
// Read in staged files | |
const gitStatus = execSync('git status --porcelain').toString() | |
const stagedFiles = [] | |
const partiallyUnstagedCache = [] | |
for (const line of gitStatus.split('\n')) { | |
const extention = line.split('.').pop() | |
if (/^A {2}/.test(line)) { | |
// Staged new file | |
stagedFiles.push(line.split('A ')[1]) | |
} else if (/^M {2}/.test(line)) { | |
// Staged modification | |
stagedFiles.push(line.split('M ')[1]) | |
} else if (/^R {2}/.test(line)) { | |
// Staged rename | |
const fileNames = line.replace('R ', '').split(' -> ') | |
stagedFiles.push(fileNames[1]) | |
} else if (/^(M|A)M /.test(line) && prettierFileTypes.includes(extention)) { | |
// A formattable file with some changes staged, some not | |
// Read in the unstaged filecontents | |
const filename = line.replace(/^(M|A)M /, '') | |
const unstagedFileContents = fs.readFileSync(filename, 'utf-8') | |
partiallyUnstagedCache.push({ | |
filename, | |
filecontents: unstagedFileContents, | |
}) | |
// Get the staged contents | |
const stagedFileContents = execSync(`git show :${filename}`).toString() | |
// Write the staged contents to the actual file | |
fs.writeFileSync(path.resolve(__dirname, filename), stagedFileContents) | |
stagedFiles.push(filename) | |
} | |
} | |
// Build up array of which files to run eslint against | |
const eslintIgnoreGlobs = readGlobsFromFile(path.resolve(__dirname, './.eslintignore')) | |
const stagedLintableFiles = stagedFiles.filter((f) => { | |
const ext = f.split('.').pop() | |
return ( | |
eslintFileTypes.includes(ext) && !micromatch.isMatch(f, eslintIgnoreGlobs) | |
) | |
}) | |
// Run the eslint command! | |
if (stagedLintableFiles.length > 0) { | |
const eslintCmd = `${path.resolve(__dirname, './node_modules/.bin/eslint')} --fix --max-warnings=0 --no-error-on-unmatched-pattern ${stagedLintableFiles.join( | |
' ' | |
)}` | |
// ...I despise that I can't just pass the whole argument as a string... | |
const eslint = spawnSync( | |
eslintCmd.split(' ')[0], | |
eslintCmd.split(' ').slice(1), | |
{ stdio: 'inherit', shell: true, cwd: __dirname } | |
) | |
if (eslint.status !== 0) { | |
// Restore the partiallyUnstagedCache | |
partiallyUnstagedCache.forEach(({ filename, filecontents }) => { | |
fs.writeFileSync(filename, filecontents) | |
}) | |
deleteLockfile() | |
process.exit(1) | |
} | |
} | |
const prettierIgnoreGlobs = readGlobsFromFile(path.resolve(__dirname, './.prettierignore')) | |
const stagedPrettierFiles = stagedFiles.filter((f) => { | |
const ext = f.split('.').pop() | |
return ( | |
prettierFileTypes.includes(ext) && | |
!micromatch.isMatch(f, prettierIgnoreGlobs) | |
) | |
}) | |
// Run prettier! (If eslint runs, this will run too) | |
if (stagedPrettierFiles.length > 0) { | |
prettier.resolveConfigFile().then((filePath) => { | |
prettier | |
.resolveConfig(filePath) | |
.then((options) => { | |
for (const stagedPrettierFile of stagedPrettierFiles) { | |
// Stage linter changes (or do nothing) | |
execSync(`git add ${stagedPrettierFile}`) | |
// Format | |
const unformattedFile = fs.readFileSync(stagedPrettierFile, 'utf-8') | |
const ext = stagedPrettierFile.split('.').pop() | |
const formattedFile = prettier.format(unformattedFile, { | |
...options, | |
parser: prettierExtParserMap[ext], | |
}) | |
fs.writeFileSync(stagedPrettierFile, formattedFile) | |
// Stage formatter changes | |
execSync(`git add ${stagedPrettierFile}`) | |
} | |
// Final step, format partially staged files, then cleanup | |
for (const { filename, filecontents } of partiallyUnstagedCache) { | |
// Format and write back to the file | |
const ext = filename.split('.').pop() | |
const formattedFile = prettier.format(filecontents, { | |
...options, | |
parser: prettierExtParserMap[ext], | |
}) | |
fs.writeFileSync(filename, formattedFile) | |
} | |
}) | |
}) | |
} | |
deleteLockfile() | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment