playwright-rs 0.20.1

Rust bindings for Microsoft Playwright
Documentation
// Pure platform-mapping and URL construction for driver acquisition.
//
// The prebuilt driver zips died with the azureedge CDN, so the driver is
// assembled from two artifacts: the `playwright-core` npm tarball and a
// pinned Node.js binary (see ADR 0006). This file is std-only so it can be
// `include!`d by `build.rs` and the cli binary, and unit-tested from the
// library suite without pulling the download deps into the lib.

/// Node.js runtime bundled with the driver. Must match the upstream
/// Playwright release's own pin for `PLAYWRIGHT_VERSION` (playwright-python
/// commits it as `NODE_VERSION` at the matching tag).
const NODE_VERSION: &str = "24.21.0";

/// Playwright platform identifier for an (os, arch) pair, e.g.
/// `("macos", "aarch64")` → `mac-arm64`. `None` for unsupported pairs.
#[allow(dead_code)]
pub(crate) fn playwright_platform(os: &str, arch: &str) -> Option<&'static str> {
    match (os, arch) {
        ("macos", "x86_64") => Some("mac"),
        ("macos", "aarch64") => Some("mac-arm64"),
        ("linux", "x86_64") => Some("linux"),
        ("linux", "aarch64") => Some("linux-arm64"),
        ("windows", "x86_64") => Some("win32_x64"),
        ("windows", "aarch64") => Some("win32_arm64"),
        _ => None,
    }
}

/// Node dist triple for a Playwright platform identifier, e.g.
/// `mac-arm64` → `darwin-arm64` (Node names platforms differently).
#[allow(dead_code)]
pub(crate) fn node_triple(playwright_platform: &str) -> Option<&'static str> {
    match playwright_platform {
        "mac" => Some("darwin-x64"),
        "mac-arm64" => Some("darwin-arm64"),
        "linux" => Some("linux-x64"),
        "linux-arm64" => Some("linux-arm64"),
        "win32_x64" => Some("win-x64"),
        "win32_arm64" => Some("win-arm64"),
        _ => None,
    }
}

/// Whether a Playwright platform identifier is a Windows target (Node ships
/// a `.zip` with `node.exe` there; `.tar.gz` with `bin/node` elsewhere).
#[allow(dead_code)]
pub(crate) fn is_windows_platform(playwright_platform: &str) -> bool {
    playwright_platform.starts_with("win32")
}

/// npm registry URL of the `playwright-core` tarball for a driver version.
#[allow(dead_code)]
fn npm_core_url(driver_version: &str) -> String {
    format!("https://registry.npmjs.org/playwright-core/-/playwright-core-{driver_version}.tgz")
}

/// nodejs.org URL of the Node binary archive for a Node dist triple.
#[allow(dead_code)]
fn node_archive_url(node_version: &str, triple: &str) -> String {
    let ext = if triple.starts_with("win-") {
        "zip"
    } else {
        "tar.gz"
    };
    format!("https://nodejs.org/dist/v{node_version}/node-v{node_version}-{triple}.{ext}")
}

/// Name of the runtime binary inside a driver directory for a Playwright
/// platform identifier: `node.exe` on Windows targets, `node` elsewhere.
///
/// The one place this is spelled out. The build script, the assembler, and
/// the runtime lookup all probe for this file, and they must agree.
pub(crate) fn node_exe_name(playwright_platform: &str) -> &'static str {
    if is_windows_platform(playwright_platform) {
        "node.exe"
    } else {
        "node"
    }
}

/// Name of an assembled driver's directory,
/// `playwright-<version>-<platform>-node<node>`, wherever it is put. The Node
/// version is part of it because the driver bundles its runtime: without it,
/// moving [`NODE_VERSION`] would leave every directory already assembled
/// running the old Node.
pub(crate) fn driver_dir_name(version: &str, platform: &str) -> String {
    format!("playwright-{version}-{platform}-node{NODE_VERSION}")
}

/// Where an assembled driver lives under a cache root:
/// `<root>/playwright-rust/<version>/<driver_dir_name>`.
///
/// One layout shared by the build script (which assembles there by
/// default), the CLI (`playwright-rs install`), and the runtime lookup, so
/// a driver produced by any of them is found by the others.
pub(crate) fn cached_driver_dir(
    cache_root: &std::path::Path,
    version: &str,
    platform: &str,
) -> std::path::PathBuf {
    cache_root
        .join("playwright-rust")
        .join(version)
        .join(driver_dir_name(version, platform))
}

/// Path of the `node` executable inside the Node dist archive.
#[allow(dead_code)]
fn node_archive_exe_path(node_version: &str, triple: &str) -> String {
    if triple.starts_with("win-") {
        format!("node-v{node_version}-{triple}/node.exe")
    } else {
        format!("node-v{node_version}-{triple}/bin/node")
    }
}

#[cfg(test)]
mod driver_urls_tests {
    use super::*;

    const ALL: [(&str, &str, &str, &str); 6] = [
        ("macos", "x86_64", "mac", "darwin-x64"),
        ("macos", "aarch64", "mac-arm64", "darwin-arm64"),
        ("linux", "x86_64", "linux", "linux-x64"),
        ("linux", "aarch64", "linux-arm64", "linux-arm64"),
        ("windows", "x86_64", "win32_x64", "win-x64"),
        ("windows", "aarch64", "win32_arm64", "win-arm64"),
    ];

    #[test]
    fn every_supported_target_maps_to_platform_and_triple() {
        for (os, arch, platform, triple) in ALL {
            assert_eq!(playwright_platform(os, arch), Some(platform));
            assert_eq!(node_triple(platform), Some(triple));
        }
        assert_eq!(playwright_platform("freebsd", "x86_64"), None);
        assert_eq!(node_triple("solaris"), None);
    }

    #[test]
    fn windows_platforms_are_detected() {
        assert!(is_windows_platform("win32_x64"));
        assert!(is_windows_platform("win32_arm64"));
        assert!(!is_windows_platform("mac-arm64"));
        assert!(!is_windows_platform("linux"));
    }

    #[test]
    fn npm_url_matches_registry_scheme() {
        assert_eq!(
            npm_core_url("1.61.1"),
            "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz"
        );
    }

    #[test]
    fn node_urls_and_exe_paths_per_family() {
        assert_eq!(
            node_archive_url("24.17.0", "darwin-arm64"),
            "https://nodejs.org/dist/v24.17.0/node-v24.17.0-darwin-arm64.tar.gz"
        );
        assert_eq!(
            node_archive_url("24.17.0", "win-x64"),
            "https://nodejs.org/dist/v24.17.0/node-v24.17.0-win-x64.zip"
        );
        assert_eq!(
            node_archive_exe_path("24.17.0", "linux-x64"),
            "node-v24.17.0-linux-x64/bin/node"
        );
        assert_eq!(
            node_archive_exe_path("24.17.0", "win-arm64"),
            "node-v24.17.0-win-arm64/node.exe"
        );
    }

    #[test]
    fn the_runtime_binary_is_node_exe_only_on_windows_targets() {
        assert_eq!(node_exe_name("win32_x64"), "node.exe");
        assert_eq!(node_exe_name("win32_arm64"), "node.exe");
        assert_eq!(node_exe_name("mac-arm64"), "node");
        assert_eq!(node_exe_name("linux"), "node");
    }

    #[test]
    fn cached_driver_dir_is_keyed_by_version_platform_and_node() {
        assert_eq!(
            cached_driver_dir(std::path::Path::new("/c"), "1.63.0", "mac-arm64"),
            std::path::PathBuf::from(format!(
                "/c/playwright-rust/1.63.0/playwright-1.63.0-mac-arm64-node{NODE_VERSION}"
            ))
        );
    }

    #[test]
    fn node_pin_is_a_bare_semver() {
        // The xtask drift guard and the nodejs.org URL scheme both assume a
        // bare MAJOR.MINOR.PATCH (no leading 'v').
        let parts: Vec<&str> = NODE_VERSION.split('.').collect();
        assert_eq!(parts.len(), 3, "NODE_VERSION must be MAJOR.MINOR.PATCH");
        assert!(parts.iter().all(|p| p.parse::<u32>().is_ok()));
    }
}