Skip to content

feat(npm): apply biome filter when package.json script invokes biome#1492

Open
LukaPrebil wants to merge 2 commits intortk-ai:developfrom
LukaPrebil:feat/1489-detect-biome-in-npm-scripts
Open

feat(npm): apply biome filter when package.json script invokes biome#1492
LukaPrebil wants to merge 2 commits intortk-ai:developfrom
LukaPrebil:feat/1489-detect-biome-in-npm-scripts

Conversation

@LukaPrebil
Copy link
Copy Markdown

Summary

  • Depends on fix(rewrite): route npm lint scripts through rtk npm (biome-safe) #1491 (please merge that first). After fix(rewrite): route npm lint scripts through rtk npm (biome-safe) #1491, npm run lint on a Biome project no longer triggers the ESLint JSON-parse bug, but the generic filter_npm_output leaves biome's Checked N files chrome in place. This PR teaches rtk npm run <script> to peek at the script body in package.json and chain the biome TOML filter when the script unambiguously invokes biome.
  • New src/cmds/js/script_detect.rs with tempfile-backed tests (no set_current_dir races): detect_script_tool / detect_script_tool_in, conservative token-level parsing. Returns None for mixed-tool scripts, substring matches, unknown bodies, and malformed JSON - no silent misclassification.
  • Also extends filter_npm_output to strip the bare > <script-body> banner npm echoes before handing off. Both npm banner lines are now stripped uniformly; downstream filters see clean tool output.

Test plan

  • cargo fmt --all && cargo clippy --all-targets && cargo test
  • Manual testing: rtk <command> output inspected

Stacked PR: this branch sits on top of #1491. The diff shows both commits until #1491 merges; only 77022fe belongs to this PR. After #1491 merges, this PR's diff will naturally shrink to just that commit.

Important: All PRs must target the develop branch (not master).
See CONTRIBUTING.md for details.

`npm run lint`, `npm lint`, `npm [rum|urn|x] lint`, and `npx lint`
previously rewrote to `rtk lint`, which defaults to running ESLint via
`npx eslint`. On Biome projects the JSON parse fails silently with exit
0, hiding real lint failures from agents.

Route these through `rtk npm` / `rtk npx` instead - the generic npm
filter is script-agnostic and preserves biome's real output and exit
code. Mirrors rtk-ai#678's pattern for the pnpm side.

Scope:
- new early-return block in rewrite_segment_inner handles the npm/npx
  `lint` variants with env prefix and redirect suffix preserved
- `split_npm_dispatch` + `match_lint_script` helpers keep the block
  compact
- npm rule pattern extended with `|lint` + `(\s|$)` suffix so bare
  `npm lint` still classifies as `rtk npm`
- `npm lint` / `npm run lint` / `npm run-script lint` / `npm rum|urn lint`
  / `npx lint` removed from lint rule's rewrite_prefixes
- 12 new unit tests (incl. env-prefix, redirect, word-boundary
  regression against `npm lint-staged`, and asserts that bare biome /
  eslint still route to rtk lint)
- `test_classify_lint` intentionally unchanged: classify still picks
  the lint rule (last-match wins), the rewrite now short-circuits to
  rtk npm/npx. Documented classify/rewrite drift.

Out of scope (filed separately):
- biome check routing through the biome TOML filter
- rtk lint exit-0 on auto-defaulted ESLint JSON parse failure
- package.json-aware filter dispatch for npm run scripts
Stacks on rtk-ai#1491. After that PR, `npm run lint` on a Biome project no
longer triggers the ESLint JSON-parse bug, but the generic
filter_npm_output leaves biome's `Checked N files` chrome in place.

This PR teaches `rtk npm run <script>` to peek at the script body in
package.json and chain the corresponding tool filter when the tool is
unambiguous:

- new src/cmds/js/script_detect.rs with a tempfile-backed test suite:
  detect_script_tool / detect_script_tool_in, conservative token-level
  parsing (returns None for mixed-tool scripts, substring matches,
  unknown bodies, malformed JSON)
- wire detection in npm_cmd::run: when the script is biome-only, chain
  filter_npm_output -> toml_filter::apply_filter(biome). Everything else
  keeps the filter_npm_output-only path (correctness-preserving fallback)
- extend filter_npm_output to also strip the bare `> <script-body>`
  banner npm emits before running the script. Previously we only
  stripped the `> pkg@ver <script>` variant. Both lines are npm's own
  echoes; downstream filters (e.g. biome.toml) only recognise bare
  tool output and need the npm chrome gone to reach their on_empty
  fallback
- 18 new detector tests + 3 filter-chain tests

Scope is biome-only by design. Adding eslint/prettier/tsc detection is
additive: a new ScriptTool variant plus a filter hookup per tool.
Tracked as a follow-up issue.
@LukaPrebil
Copy link
Copy Markdown
Author

Smoke test (PR 1 alone vs PR 1 + PR 2)

Scratch package.json with "lint": "biome check", biome mocked to emit a clean Checked N files summary, then again to emit a real diagnostic.

Clean pass

PR 1 only (#1491) PR 1 + PR 2 (this PR)
Output > lint
> biome check
Checked 42 files in 0.3s
biome: ok
Exit 0 0

Error pass

PR 1 only (#1491) PR 1 + PR 2 (this PR)
Output > lint
> biome check
Checked 42 files in 0.5s
./src/app.ts:5:3 lint/style/useConst FIXABLE
× This variable can be declared as const.
Found 1 error.
./src/app.ts:5:3 lint/style/useConst FIXABLE
× This variable can be declared as const.
Found 1 error.
Exit 1 1

ESLint project (detector returns None, falls back to filter_npm_output)

No regression vs PR 1 alone — ESLint diagnostic lines preserved, npm banner stripped (the filter_npm_output extension benefits this path too), exit 1 propagates.

Follow-ups enabled by this PR

The ScriptTool enum is the natural extension point. Adding Eslint, Prettier, or Tsc variants is a per-tool PR:

  1. Add the variant.
  2. Add a branch in detect_tool_in_script that promotes the existing token detection to Some(ScriptTool::Xxx).
  3. Add a filter hookup in npm_cmd::run's match arm.

Each tool can ship independently behind its own filter availability. eslint.toml and prettier.toml don't exist yet in src/filters/, so those wirings need a text filter added first.

Why strip the bare > <script-body> banner in filter_npm_output?

Without it, the chained filter for biome never reaches its on_empty fallback: the biome TOML filter's strip_lines_matching intentionally doesn't touch npm's own echoes (that's not biome's output), so lines like > biome check survive both filters and clutter the output. The extension is scoped narrowly: any line starting with > (there are two such lines in a typical npm script invocation, both emitted by npm before the script starts). Scripts that legitimately emit >-prefixed output are vanishingly rare; if this ever bites a user, we can tighten the match to ^> (\S+@\S+ |\S+) and preserve exact-match semantics.

@pszymkowiak pszymkowiak added effort-medium 1-2 jours, quelques fichiers enhancement New feature or request filter-quality Filter produces incorrect/truncated signal labels Apr 24, 2026
@pszymkowiak
Copy link
Copy Markdown
Collaborator

[w] wshm · Automated triage by AI

📊 Automated PR Analysis

Type feature
🟡 Risk medium

Summary

Teaches rtk npm run <script> to detect when a package.json script invokes biome by parsing the script body, then chains the biome TOML filter on top of the generic npm output filter. Adds a new script_detect.rs module with conservative token-level parsing and tempfile-backed tests, and extends filter_npm_output to strip the bare > <script-body> banner npm echoes.

Review Checklist

  • Tests present
  • Breaking change
  • Docs updated

Linked issues: #1489, #1491


Analyzed automatically by wshm · This is an automated analysis, not a human review.

Comment thread src/discover/rules.rs
"pnpm eslint",
"pnpm exec biome",
"pnpm exec eslint",
"pnpm lint",
Copy link
Copy Markdown
Collaborator

@KuSh KuSh Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't leave pnpm/pnpx alone, it sure have the same problem

Comment on lines +37 to +57
fn detect_tool_in_script(body: &str) -> Option<ScriptTool> {
let has_biome = has_tool_token(body, "biome");
let has_eslint = has_tool_token(body, "eslint");
let has_prettier = has_tool_token(body, "prettier");
let has_tsc = has_tool_token(body, "tsc");

let tools_found = [has_biome, has_eslint, has_prettier, has_tsc]
.iter()
.filter(|b| **b)
.count();
if tools_found != 1 {
return None;
}

if has_biome {
Some(ScriptTool::Biome)
} else {
// Recognised but no filter wired yet (eslint / prettier / tsc).
None
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to be required. We can directly call toml_filter::find_matching_filter on it

Comment thread src/cmds/js/npm_cmd.rs
match script_name.and_then(detect_script_tool) {
Some(ScriptTool::Biome) => {
// Biome TOML filter matches `^biome\b`.
if let Some(biome) = toml_filter::find_matching_filter("biome") {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're not respecting the RTK_NO_TOML env variable that disable toml filters.

Also this should not just call toml filters, it should also check recursively if the command match any rtk natively handled command (that's not something we allow ATM), if vitest or jest are behind the lint script we should call rtk vitest/rtk jest

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort-medium 1-2 jours, quelques fichiers enhancement New feature or request filter-quality Filter produces incorrect/truncated signal

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants