frame-cli 0.3.0

CLI for Frame — five intention-verbs over one application: frame new scaffolds it, frame run serves it, frame test proves it (real browser included), frame check verifies it statically, frame doctor walks the prerequisites
Documentation
//! Shared scaffold-acceptance test support: temp-directory management and
//! generated-tree inspection, used by every `tests/*.rs` integration binary
//! in this crate (`tests/scaffold.rs`, `tests/vocabulary.rs`) via
//! `mod support;`. Command-running and toolchain-gate helpers stay local to
//! `scaffold.rs`: `vocabulary.rs` only reads generated bytes, it never
//! shells out, so sharing those helpers here would leave them unused (and
//! `-D warnings`-fatal) in `vocabulary.rs`'s own compilation.

use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};

pub use frame_cli::{NewOptions, generate};

use std::sync::atomic::{AtomicU64, Ordering};

static SEQUENCE: AtomicU64 = AtomicU64::new(0);

/// A uniquely named, auto-removed scratch directory for one generation.
pub struct TestDirectory(PathBuf);

impl TestDirectory {
    /// Creates a fresh, empty scratch directory under the system temp root.
    pub fn new(label: &str) -> Result<Self, std::io::Error> {
        let sequence = SEQUENCE.fetch_add(1, Ordering::Relaxed);
        let path = std::env::temp_dir().join(format!(
            "frame-cli-{label}-{}-{sequence}",
            std::process::id()
        ));
        remove_if_present(&path)?;
        fs::create_dir_all(&path)?;
        Ok(Self(path))
    }

    /// The scratch directory's path.
    pub fn path(&self) -> &Path {
        &self.0
    }
}

impl Drop for TestDirectory {
    fn drop(&mut self) {
        if let Err(error) = remove_if_present(&self.0) {
            eprintln!("failed to remove {}: {error}", self.0.display());
        }
    }
}

/// Builds `NewOptions` for a generation rooted at `parent`.
pub fn options(parent: &Path, name: &str) -> NewOptions {
    NewOptions {
        name: name.to_owned(),
        target_parent: parent.to_path_buf(),
    }
}

/// Reads every file under `root` (excluding build/dependency output) into a
/// deterministic, sorted map of relative path to bytes.
pub fn generated_files(root: &Path) -> Result<BTreeMap<PathBuf, Vec<u8>>, std::io::Error> {
    let mut files = BTreeMap::new();
    collect_files(root, root, &mut files)?;
    Ok(files)
}

fn collect_files(
    root: &Path,
    directory: &Path,
    files: &mut BTreeMap<PathBuf, Vec<u8>>,
) -> Result<(), std::io::Error> {
    let mut entries = fs::read_dir(directory)?.collect::<Result<Vec<_>, _>>()?;
    entries.sort_by_key(std::fs::DirEntry::file_name);
    for entry in entries {
        let path = entry.path();
        // `page/dist` is scaffold-authored servable content (index.html,
        // styles.css, vendor/) plus `tsc`'s deterministic compiled modules —
        // it is generated output and MUST be scanned. Any other `dist`
        // stays excluded as build/dependency output, alongside
        // target/build/node_modules.
        let is_page_dist = path.file_name().is_some_and(|name| name == "dist")
            && path
                .parent()
                .and_then(Path::file_name)
                .is_some_and(|name| name == "page");
        if (path.file_name().is_some_and(|name| {
            name == "target" || name == "build" || name == "node_modules" || name == "dist"
        }) && !is_page_dist)
            || path.file_name().is_some_and(|name| {
                name == "Cargo.lock" || name == "manifest.toml" || name == "package-lock.json"
            })
        {
            continue;
        }
        if path.is_dir() {
            collect_files(root, &path, files)?;
        } else {
            let relative = path
                .strip_prefix(root)
                .map_err(std::io::Error::other)?
                .to_path_buf();
            files.insert(relative, fs::read(path)?);
        }
    }
    Ok(())
}

/// Removes `path` if present; a not-found path is not an error.
pub fn remove_if_present(path: &Path) -> Result<(), std::io::Error> {
    match fs::remove_dir_all(path) {
        Ok(()) => Ok(()),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
        Err(error) => Err(error),
    }
}