hni 0.0.3

ni-compatible package manager command router with node shim
Documentation
use std::fs;

mod support;

use support::run_hni;

struct PmCase {
    label: &'static str,
    package_manager: &'static str,
    lockfile: &'static str,
    local_bins: bool,
    yarn_pnp: bool,
    expected_nr: &'static str,
    expected_nlx: &'static str,
}

#[test]
fn fast_mode_matrix_covers_supported_and_fallback_package_managers() {
    if cfg!(windows) {
        return;
    }

    support::with_env_lock(|| {
        let cases = [
            PmCase {
                label: "npm",
                package_manager: "npm@10.0.0",
                lockfile: "package-lock.json",
                local_bins: true,
                yarn_pnp: false,
                expected_nr: "hni fast:run-script dev",
                expected_nlx: "hni fast:run-local-bin hello --flag",
            },
            PmCase {
                label: "pnpm",
                package_manager: "pnpm@9.0.0",
                lockfile: "pnpm-lock.yaml",
                local_bins: true,
                yarn_pnp: false,
                expected_nr: "hni fast:run-script dev",
                expected_nlx: "hni fast:run-local-bin hello --flag",
            },
            PmCase {
                label: "yarn-classic",
                package_manager: "yarn@1.22.0",
                lockfile: "yarn.lock",
                local_bins: true,
                yarn_pnp: false,
                expected_nr: "hni fast:run-script dev",
                expected_nlx: "hni fast:run-local-bin hello --flag",
            },
            PmCase {
                label: "yarn-berry-node-modules",
                package_manager: "yarn@4.0.0",
                lockfile: "yarn.lock",
                local_bins: true,
                yarn_pnp: false,
                expected_nr: "hni fast:run-script dev",
                expected_nlx: "hni fast:run-local-bin hello --flag",
            },
            PmCase {
                label: "yarn-berry-pnp",
                package_manager: "yarn@4.0.0",
                lockfile: "yarn.lock",
                local_bins: false,
                yarn_pnp: true,
                expected_nr: "yarn run dev",
                expected_nlx: "yarn dlx hello --flag",
            },
            PmCase {
                label: "bun",
                package_manager: "bun@1.1.0",
                lockfile: "bun.lockb",
                local_bins: true,
                yarn_pnp: false,
                expected_nr: "hni fast:run-script dev",
                expected_nlx: "hni fast:run-local-bin hello --flag",
            },
            PmCase {
                label: "deno",
                package_manager: "deno@1.46.0",
                lockfile: "deno.lock",
                local_bins: false,
                yarn_pnp: false,
                expected_nr: "deno task dev",
                expected_nlx: "deno run npm:hello --flag",
            },
        ];

        for case in cases {
            let work = tempfile::tempdir().unwrap();
            let project = work.path().join(case.label);
            fs::create_dir_all(&project).unwrap();
            fs::write(project.join(case.lockfile), "lock\n").unwrap();
            fs::write(
                project.join("package.json"),
                format!(
                    r#"{{"name":"{}","packageManager":"{}","scripts":{{"dev":"echo ok"}}}}"#,
                    case.label, case.package_manager
                ),
            )
            .unwrap();

            if case.yarn_pnp {
                fs::write(project.join(".pnp.cjs"), "module.exports = {};\n").unwrap();
            }

            if case.local_bins {
                let bin_dir = project.join("node_modules").join(".bin");
                fs::create_dir_all(&bin_dir).unwrap();
                let bin = bin_dir.join("hello");
                fs::write(&bin, "#!/bin/sh\n").unwrap();
                make_executable(&bin);
            }

            let nr = run_hni(
                vec![
                    "nr",
                    "-C",
                    project.to_str().unwrap(),
                    "--fast",
                    "--debug-resolved",
                    "dev",
                ],
                &[("HNI_SKIP_PM_CHECK", "1")],
            );
            assert!(
                nr.status.success(),
                "nr failed for {}: {}",
                case.label,
                String::from_utf8_lossy(&nr.stderr)
            );
            assert_eq!(String::from_utf8_lossy(&nr.stdout).trim(), case.expected_nr);

            let nlx = run_hni(
                vec![
                    "nlx",
                    "-C",
                    project.to_str().unwrap(),
                    "--fast",
                    "--debug-resolved",
                    "hello",
                    "--flag",
                ],
                &[("HNI_SKIP_PM_CHECK", "1")],
            );
            assert!(
                nlx.status.success(),
                "nlx failed for {}: {}",
                case.label,
                String::from_utf8_lossy(&nlx.stderr)
            );
            assert_eq!(
                String::from_utf8_lossy(&nlx.stdout).trim(),
                case.expected_nlx
            );
        }
    });
}

#[cfg(unix)]
fn make_executable(path: &std::path::Path) {
    use std::os::unix::fs::PermissionsExt;

    let mut perms = fs::metadata(path).unwrap().permissions();
    perms.set_mode(0o755);
    fs::set_permissions(path, perms).unwrap();
}

#[cfg(not(unix))]
fn make_executable(_path: &std::path::Path) {}