1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
//! pre-commit-lint-js — ESLint over staged JS/TS, scoped to repos that lint.
use super::common::{
fail, first_existing, hl, ok, repo_root, resolve_tool, run as run_tool, staged_files, warn,
};
use crate::check::Outcome;
/// The extensions this check consumes. Exported so `registry.rs` declares the
/// scope from the same constant — see `lint_json_yaml::EXTS` for the drift this
/// prevents.
pub const EXTS: &[&str] = &[".js", ".jsx", ".ts", ".tsx", ".vue"];
pub fn run(args: &[std::ffi::OsString]) -> Outcome {
let files = staged_files(EXTS);
if files.is_empty() {
return Outcome::Passed;
}
let root = repo_root();
// ESLint 9+ ERRORS OUT when it finds no config ("couldn't find an
// eslint.config file") instead of no-op'ing, which would fail commits in
// repos that do not lint JS at all — infra and manifest repos, and this
// templates repo. So the config is the opt-in signal.
let has_config = first_existing(
&root,
&[
"eslint.config.js",
"eslint.config.mjs",
"eslint.config.cjs",
"eslint.config.ts",
".eslintrc",
".eslintrc.js",
".eslintrc.cjs",
".eslintrc.json",
".eslintrc.yml",
".eslintrc.yaml",
],
)
.is_some()
|| std::fs::read_to_string(format!("{root}/package.json"))
.map(|p| p.contains("\"eslintConfig\""))
.unwrap_or(false);
if !has_config {
ok("ESLint skipped (no eslint config)");
return Outcome::Passed;
}
let Some(argv) = resolve_tool(&root, "eslint") else {
// A non-zero exit from a MISSING eslint is not the same as lint errors,
// so warn and skip rather than block with a false "issues found".
warn(&format!(
"eslint config found but no eslint binary. Run {}",
hl("npm install")
));
return Outcome::Unavailable;
};
// Forwarded flags first, THEN `--`, THEN the files: a staged file named
// e.g. `-x.js` would otherwise be read as a flag by eslint's own parser,
// and putting `--` before the forwarded args instead would not separate
// the files from anything — it would only separate ITSELF from `args`.
let mut extra: Vec<String> = args
.iter()
.filter_map(|a| a.to_str())
.map(str::to_owned)
.collect();
extra.push("--".to_string());
extra.extend(files);
if !run_tool(&root, &argv, &extra) {
fail("ESLint issues found. Please fix");
return Outcome::Failed;
}
ok("ESLint passed");
Outcome::Passed
}