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
//! `frame build` — build the application without booting it. The quiet
//! alias behind `frame run`'s build phase.

use std::path::PathBuf;

use crate::doctor;
use crate::error::CliError;
use crate::project::{App, run_step};

/// Builds the application: recompiles the page only when its sources
/// changed, then builds the host binary (cargo's own incremental judgment
/// applies there).
///
/// # Errors
///
/// Refuses on missing prerequisites and fails on any build failure.
pub fn build() -> Result<(), CliError> {
    let app = App::find_from_current_dir()?;
    doctor::require(doctor::BUILD_PREREQUISITES, "frame build")?;
    build_application(&app)?;
    eprintln!("built {}", host_binary(&app).display());
    Ok(())
}

/// The shared build phase: page-if-stale, then the host binary.
///
/// # Errors
///
/// Fails on a stale page without its toolchain, or any build failure.
pub fn build_application(app: &App) -> Result<(), CliError> {
    build_page_if_stale(app)?;
    run_step("cargo", &["build", "-p", "app-host"], &app.root)
}

/// Recompiles the page when (and only when) sources changed. A fresh
/// scaffold ships its compiled page, so this needs npm only after the first
/// real edit — and refuses loudly, with the fix, if that toolchain is
/// missing then.
///
/// # Errors
///
/// Fails on a stale page without its toolchain, or a failing compile.
pub fn build_page_if_stale(app: &App) -> Result<(), CliError> {
    let stale = app.stale_page_sources()?;
    if stale.is_empty() {
        eprintln!("page: compiled modules are current");
        return Ok(());
    }
    if !app.page_toolchain_installed() {
        return Err(CliError::PageToolchainMissing {
            changed: stale.len(),
        });
    }
    eprintln!("page: {} source(s) changed — compiling (tsc)…", stale.len());
    run_step("npm", &["run", "build"], &app.page_dir())
}

/// Where the built host binary lands: `CARGO_TARGET_DIR` when set (resolved
/// against the application root, exactly as cargo resolves it against its
/// own working directory), the application's own `target/` otherwise.
#[must_use]
pub fn host_binary(app: &App) -> PathBuf {
    let target_root = std::env::var_os("CARGO_TARGET_DIR").map_or_else(
        || app.root.join("target"),
        |directory| app.root.join(directory),
    );
    target_root.join("debug").join("app-host")
}