Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 132 additions & 32 deletions src/discover/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3108,12 +3108,18 @@ mod tests {
);
}

// Bare `pnpm build` is still NOT rewritten: it would only hit the passthrough
// (no output parser), so rewriting it would add false-positive surface for zero
// savings. Stripping global opts must not change this.
// Bare `pnpm <script>` now routes to `rtk pnpm`, which runs the script
// verbatim through the passthrough. #3275 left it unrouted because the
// passthrough parses nothing, but dropping the script names from the lint
// rule (this change) means `pnpm lint` would otherwise match no rule at all
// and lose its rewrite. Routing the whole bare form keeps the script's real
// flags, chain and exit code, and the output filter is the follow-up.
#[test]
fn test_rewrite_pnpm_bare_build_none() {
assert_eq!(rewrite_command_no_prefixes("pnpm build", &[]), None);
fn test_rewrite_pnpm_bare_build_routes_to_pnpm() {
assert_eq!(
rewrite_command_no_prefixes("pnpm build", &[]),
Some("rtk pnpm build".into())
);
}

// False-positive guards.
Expand All @@ -3135,10 +3141,16 @@ mod tests {
}

#[test]
fn test_rewrite_pnpm_recursive_lint_safe_noop() {
// `pnpm lint` classifies as Supported, but the ORIGINAL `pnpm -r lint`
// matches no lint rewrite-prefix → safe no-op (never a malformed rewrite).
assert_eq!(rewrite_command_no_prefixes("pnpm -r lint", &[]), None);
fn test_rewrite_pnpm_recursive_lint_routes_to_pnpm() {
// `pnpm -r lint` used to be a safe no-op: it classified through the lint
// rule, whose rewrite prefixes never matched the flag-first spelling.
// With `lint` off that rule it classifies as `rtk pnpm` (via the global
// -opt strip) and the `pnpm` rewrite prefix does match, so `-r` is
// forwarded as the global flag `rtk pnpm` already accepts.
assert_eq!(
rewrite_command_no_prefixes("pnpm -r lint", &[]),
Some("rtk pnpm -r lint".into())
);
}

#[test]
Expand All @@ -3148,8 +3160,10 @@ mod tests {
// original flag-first text and never fires, so a Supported verdict would
// advertise savings `rtk discover`/`rtk session` can never deliver. These
// must classify exactly as on develop: Unsupported(pnpm).
// (`pnpm -r lint` left this list when `lint` left the lint rule — it now
// routes to `rtk pnpm`, and its rewrite does fire. See
// `test_rewrite_pnpm_recursive_lint_routes_to_pnpm`.)
for cmd in [
"pnpm -r lint",
"pnpm -r exec eslint .",
"pnpm --filter @app exec vitest run",
"pnpm -F web exec playwright test",
Expand Down Expand Up @@ -5482,16 +5496,12 @@ mod tests {
"npm exec eslint",
"npm rum biome",
"npm rum eslint",
"npm rum lint",
"npm run biome",
"npm run eslint",
"npm run lint",
"npm run-script biome",
"npm run-script eslint",
"npm run-script lint",
"npm urn biome",
"npm urn eslint",
"npm urn lint",
"npm x biome",
"npm x eslint",
"pnpm dlx biome",
Expand All @@ -5500,25 +5510,18 @@ mod tests {
"pnpm exec eslint",
"pnpm run biome",
"pnpm run eslint",
"pnpm run lint",
"pnpm run-script biome",
"pnpm run-script eslint",
"pnpm run-script lint",
"npm biome",
"npm eslint",
"npm lint",
"npx biome",
"npx eslint",
"npx lint",
"pnpm biome",
"pnpm eslint",
"pnpm lint",
"pnpx biome",
"pnpx eslint",
"pnpx lint",
"biome",
"eslint",
"lint",
];
for command in commands {
assert!(
Expand All @@ -5535,23 +5538,49 @@ mod tests {
}
}

#[test]
fn test_classify_lint_scripts_not_rewritten() {
let script_commands = vec![
"lint",
"npm lint",
"npm run lint",
"npm rum lint",
"npm urn lint",
"npm run-script lint",
"npx lint",
"pnpm lint",
"pnpm run lint",
"pnpm run-script lint",
"pnpx lint",
];
for command in script_commands {
assert!(
!matches!(
classify_command(command),
Classification::Supported {
rtk_equivalent: "rtk lint",
..
}
),
"'{}' should NOT classify as rtk lint (it's a script, not a linter binary)",
command
);
}
}

#[test]
fn test_rewrite_lint() {
let commands = vec![
"npm exec biome",
"npm exec eslint",
"npm rum biome",
"npm rum eslint",
"npm rum lint",
"npm run biome",
"npm run eslint",
"npm run lint",
"npm run-script biome",
"npm run-script eslint",
"npm run-script lint",
"npm urn biome",
"npm urn eslint",
"npm urn lint",
"npm x biome",
"npm x eslint",
"pnpm dlx biome",
Expand All @@ -5560,25 +5589,18 @@ mod tests {
"pnpm exec eslint",
"pnpm run biome",
"pnpm run eslint",
"pnpm run lint",
"pnpm run-script biome",
"pnpm run-script eslint",
"pnpm run-script lint",
"npm biome",
"npm eslint",
"npm lint",
"npx biome",
"npx eslint",
"npx lint",
"pnpm biome",
"pnpm eslint",
"pnpm lint",
"pnpx biome",
"pnpx eslint",
"pnpx lint",
"biome",
"eslint",
"lint",
];
for command in commands {
assert_eq!(
Expand All @@ -5590,6 +5612,84 @@ mod tests {
}
}

#[test]
fn test_rewrite_lint_scripts_not_rewritten_to_rtk_lint() {
let script_commands = vec![
"lint",
"npm lint",
"npm run lint",
"pnpm lint",
"pnpm run lint",
"npx lint",
"pnpx lint",
];
for command in script_commands {
let result = rewrite_command_no_prefixes(command, &[]);
assert_ne!(
result,
Some("rtk lint".into()),
"'{}' should NOT be rewritten to rtk lint (got {:?})",
command,
result
);
}
}

#[test]
fn test_pnpm_scripts_route_through_rtk_pnpm() {
let script_commands = vec![
("pnpm lint", "rtk pnpm lint"),
("pnpm build", "rtk pnpm build"),
("pnpm dev", "rtk pnpm dev"),
("pnpm test", "rtk pnpm test"),
("pnpm typecheck", "rtk pnpm typecheck"),
("pnpm run lint", "rtk pnpm run lint"),
("pnpm run build", "rtk pnpm run build"),
];
for (command, expected) in script_commands {
assert_eq!(
classify_command(command),
Classification::Supported {
rtk_equivalent: "rtk pnpm",
category: "PackageManager",
estimated_savings_pct: 80.0,
status: RtkStatus::Existing,
},
"'{}' should classify as rtk pnpm",
command
);
assert_eq!(
rewrite_command_no_prefixes(command, &[]),
Some(expected.into()),
"'{}' should rewrite to '{}'",
command,
expected
);
}
}

#[test]
fn test_pnpm_specific_tools_still_override_generic() {
let specific_commands = vec![
("pnpm eslint", "rtk lint"),
("pnpm biome", "rtk lint"),
("pnpm vitest", "rtk vitest"),
("pnpm jest", "rtk jest"),
("pnpm tsc", "rtk tsc"),
("pnpm prettier", "rtk prettier"),
("pnpm playwright", "rtk playwright"),
];
for (command, expected_rtk) in specific_commands {
assert_eq!(
rewrite_command_no_prefixes(command, &[]),
Some(expected_rtk.into()),
"'{}' should route to specific '{}', not generic rtk pnpm",
command,
expected_rtk
);
}
}

#[test]
fn test_classify_jest() {
let commands = vec![
Expand Down
22 changes: 9 additions & 13 deletions src/discover/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,14 @@ pub const RULES: &[RtkRule] = &[
subcmd_status: &[("fmt", RtkStatus::Passthrough)],
},
RtkRule {
pattern: r"^pnpm\s+(exec|i|install|list|ls|outdated|run|run-script)",
// Any bare `pnpm <word>` routes here, so a package.json script keeps its
// real flags, chain and exit code instead of being re-spelled as a tool
// invocation. Enumerating script names would just be another list to
// chase. The first token must not start with `-`: a global flag is not a
// subcommand, and `rtk pnpm --filter @app` / `rtk pnpm -x install` would
// die at clap. Flag-first forms keep reaching this rule through
// `strip_pnpm_global_opts` (#3275), which only strips the fixed set.
pattern: r"^pnpm\s+[^-\s]\S*",
rtk_cmd: "rtk pnpm",
rewrite_prefixes: &["pnpm"],
category: "PackageManager",
Expand Down Expand Up @@ -206,51 +213,40 @@ pub const RULES: &[RtkRule] = &[
..RtkRule::DEFAULT
},
RtkRule {
pattern: r"^((p?np(m|x)|p?npm\s+(exec|run|run-script)|npm\s+(rum|urn|x)|pnpm\s+dlx)\s+)?(biome|eslint|lint)(\s|$)",
pattern: r"^((p?np(m|x)|p?npm\s+(exec|run|run-script)|npm\s+(rum|urn|x)|pnpm\s+dlx)\s+)?(biome|eslint)(\s|$)",
rtk_cmd: "rtk lint",
pipeline_safety: PipelineSafety::ProducerOnly,
rewrite_prefixes: &[
"biome",
"eslint",
"lint",
"npm biome",
"npm eslint",
"npm exec biome",
"npm exec eslint",
"npm lint",
"npm rum biome",
"npm rum eslint",
"npm rum lint",
"npm run biome",
"npm run eslint",
"npm run lint",
"npm run-script biome",
"npm run-script eslint",
"npm run-script lint",
"npm urn biome",
"npm urn eslint",
"npm urn lint",
"npm x biome",
"npm x eslint",
"npx biome",
"npx eslint",
"npx lint",
"pnpm biome",
"pnpm dlx biome",
"pnpm dlx eslint",
"pnpm eslint",
"pnpm exec biome",
"pnpm exec eslint",
"pnpm lint",
"pnpm run biome",
"pnpm run eslint",
"pnpm run lint",
"pnpm run-script biome",
"pnpm run-script eslint",
"pnpm run-script lint",
"pnpx biome",
"pnpx eslint",
"pnpx lint",
],
category: "Build",
savings_pct: 84.0,
Expand Down
Loading