Skip to content

Instantly share code, notes, and snippets.

@n8jadams
Last active July 8, 2021 17:15
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 n8jadams/931f2241a193f7ea1227205cf20ef9f1 to your computer and use it in GitHub Desktop.
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)
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