npmgen-core 0.3.1

Library that generates the npm publish tree shipping a prebuilt Rust binary.
Documentation
//! Generates the launcher shim bundled in the meta package.
//!
//! The script is project-agnostic: at run time it reads its own `package.json`,
//! computes `<platform>-<arch>`, finds the matching `optionalDependency`,
//! resolves the binary, and execs it, relaying its exit status. Only the
//! missing/failure policy varies.

/// Renders the launcher JavaScript.
#[derive(Debug)]
pub struct LauncherScript {
    fail_open: bool,
}

impl LauncherScript {
    pub fn new(fail_open: bool) -> Self {
        Self { fail_open }
    }

    /// The launcher source. A resolution or spawn failure is routed through
    /// `missing`: with `fail_open` it exits 0 (for hooks that must not block);
    /// otherwise it reports to stderr and exits 1. A normal run relays the
    /// child's exit status, and a signal death (null status) maps to the failure
    /// code rather than success.
    pub fn render(&self) -> String {
        let on_missing = if self.fail_open {
            "  process.exit(0);"
        } else {
            "  process.stderr.write(`npmgen launcher: ${reason}\\n`);\n  process.exit(1);"
        };
        let failure_exit = if self.fail_open { "0" } else { "1" };
        format!(
            r#"// Generated by npmgen. Runs the platform-specific binary for this package.
import {{ spawnSync }} from "node:child_process";
import {{ createRequire }} from "node:module";
import {{ readFileSync }} from "node:fs";
import {{ fileURLToPath }} from "node:url";
import {{ dirname, join }} from "node:path";

const root = dirname(fileURLToPath(import.meta.url));
const key = `${{process.platform}}-${{process.arch}}`;
const ext = process.platform === "win32" ? ".exe" : "";

function missing(reason) {{
{on_missing}
}}

let pkg;
try {{
  pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
}} catch {{
  missing("cannot read package.json");
}}

const dependency = Object.keys(pkg.optionalDependencies ?? {{}}).find((name) => name.endsWith(`-${{key}}`));
const binaryName = (pkg.name ?? "").split("/").pop();
if (!dependency || !binaryName) {{
  missing(`no package for ${{key}}`);
}}

const require = createRequire(import.meta.url);
let binary;
try {{
  binary = require.resolve(`${{dependency}}/${{binaryName}}${{ext}}`);
}} catch {{
  missing(`binary for ${{key}} is not installed`);
}}

const result = spawnSync(binary, process.argv.slice(2), {{ stdio: "inherit" }});
if (result.error) {{
  missing(`failed to run binary: ${{result.error.message}}`);
}}
// A signal death leaves status null; treat it as failure, never success.
process.exit(result.status ?? {failure_exit});
"#
        )
    }
}

#[cfg(test)]
mod tests {
    use super::LauncherScript;

    #[test]
    fn fail_open_exits_zero_on_any_failure() {
        let source = LauncherScript::new(true).render();
        assert!(source.contains("process.exit(0);"));
        assert!(!source.contains("process.exit(1);"));
        assert!(source.contains("result.status ?? 0"));
        assert!(source.contains("if (result.error)"));
        assert!(source.contains("spawnSync"));
    }

    #[test]
    fn fail_hard_reports_and_exits_nonzero() {
        let source = LauncherScript::new(false).render();
        assert!(source.contains("process.exit(1);"));
        assert!(source.contains("process.stderr.write"));
        // A failed spawn or null (signal) status must not coalesce to success.
        assert!(source.contains("if (result.error)"));
        assert!(source.contains("result.status ?? 1"));
        assert!(!source.contains("result.status ?? 0"));
    }
}