How llmfit Embeds the React dist/ Folder into the Binary via include_str! in the Build Script

The llmfit-tui crate uses a custom Cargo build script to traverse the llmfit-web/dist directory and generate a Rust module containing include_bytes! (and include_str! for text assets) macros, compiling the entire React dashboard directly into the final executable as static byte slices.

The AlexsJones/llmfit repository solves the challenge of shipping a web-based UI with a Rust CLI by embedding the compiled React frontend directly into the binary. At compile time, the llmfit-tui/build.rs script locates the build output from the llmfit-web package and generates static asset modules that become part of the final executable, eliminating runtime file system dependencies.

Locating the Web Assets Relative to the Crate

The build script begins by resolving the path to the React build output relative to the llmfit-tui crate root. Since the llmfit-web package typically sits at the workspace root, the script constructs a path pointing to ../llmfit-web/dist from the CARGO_MANIFEST_DIR.

// llmfit-tui/build.rs
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let dist_dir = Path::new(&manifest_dir)
    .join("..")
    .join("llmfit-web")
    .join("dist");

This approach ensures the build script can locate the minified JavaScript, CSS, and HTML files regardless of where the build command is invoked from within the workspace.

Detecting Changes with cargo:rerun-if-changed

To ensure the binary rebuilds whenever the React frontend changes, the script emits a rerun-if-changed directive for the entire dist/ directory. This Cargo instruction monitors all files recursively within the folder.

// llmfit-tui/build.rs
println!("cargo:rerun-if-changed={}", dist_dir.display());

When any file in llmfit-web/dist is modified, Cargo automatically re-executes the build script before the next compilation, keeping the embedded assets synchronized with the latest web build.

Generating Static Asset Modules with include_bytes! and include_str!

The core embedding mechanism involves generating a temporary Rust source file—generated_assets.rs—that contains static references to each file in the dist/ folder. The build script walks the directory tree and creates a static byte slice for each asset using include_bytes! for binary files (images, fonts) and include_str! for text files (HTML, JS, CSS).

// llmfit-tui/build.rs (simplified generation logic)
fn generate_assets_from_dist(dist_path: &Path, files: &[PathBuf]) -> String {
    let mut output = String::new();
    
    for file in files {
        let relative_path = file.strip_prefix(dist_path).unwrap();
        let var_name = path_to_var_name(relative_path);
        
        if is_text_file(file) {
            // Use include_str! for HTML, CSS, and JS files
            output.push_str(&format!(
                "pub const {}: &str = include_str!(r#\"{}\"#);\n",
                var_name,
                file.display()
            ));
        } else {
            // Use include_bytes! for binary assets
            output.push_str(&format!(
                "pub const {}: &[u8] = include_bytes!(r#\"{}\"#);\n",
                var_name,
                file.display()
            ));
        }
    }
    output
}

The generated file is written to the source directory (typically src/generated_assets.rs) and then included in the main library via the include! macro or by declaring it as a module.

Fallback Handling for Missing Web Builds

When the dist/ directory does not exist—such as during a fresh clone where npm run build hasn't been executed—the build script implements a fallback mechanism. Instead of failing, it embeds a minimal placeholder page defined as a constant (FALLBACK_JS or similar) and emits a Cargo warning to alert the developer.

// llmfit-tui/build.rs
if !dist_dir.is_dir() {
    println!("cargo:warning=llmfit-web/dist not found. Using fallback page.");
    println!("cargo:warning=Run 'npm ci && npm run build' in llmfit-web/ to embed the dashboard.");
    
    // Generate fallback content
    let fallback = r#"<!DOCTYPE html><html><body>Dashboard not built</body></html>"#;
    std::fs::write("src/generated_assets.rs", format!(
        "pub const INDEX_HTML: &str = {:?};",
        fallback
    )).unwrap();
}

This ensures the Rust crate always compiles successfully, even without the Node.js build environment, while clearly indicating that the web UI is unavailable.

Serving Embedded Assets at Runtime

At runtime, the HTTP server implemented in llmfit-tui/src/serve_api.rs serves these embedded files directly from memory. Since the assets are compiled as &'static [u8] or &'static str references, they require no file system I/O and remain available even in containerized or restricted environments.

// llmfit-tui/src/serve_api.rs (conceptual)
use crate::generated_assets;

fn serve_static(path: &str) -> Response {
    match path {
        "/" => Response::html(generated_assets::INDEX_HTML),
        "/index.js" => Response::javascript(generated_assets::INDEX_JS),
        "/style.css" => Response::css(generated_assets::STYLE_CSS),
        _ => Response::not_found(),
    }
}

Because the assets are embedded at compile time, the resulting llmfit binary is completely self-contained, shipping both the TUI logic and the React dashboard in a single executable file.

Summary

  • The llmfit-tui/build.rs script automates the embedding of React build artifacts by generating a generated_assets.rs module at compile time.
  • include_bytes! handles binary assets (images, fonts), while include_str! handles text-based assets (HTML, CSS, JavaScript), ensuring proper static typing for each file type.
  • cargo:rerun-if-changed ensures the binary rebuilds automatically whenever files in llmfit-web/dist are modified.
  • A fallback mechanism allows the crate to compile even when the web build is absent, emitting developer warnings instead of hard failures.
  • The serve_api.rs module serves these static slices directly from memory, creating a zero-dependency runtime deployment.

Frequently Asked Questions

What happens if the llmfit-web/dist folder is missing during compilation?

The build script detects the missing directory and falls back to a minimal placeholder page defined in the FALLBACK_JS constant or similar inline string. It prints a cargo:warning message instructing the developer to run npm ci && npm run build in the llmfit-web/ directory, but the Rust compilation succeeds with limited functionality.

How does the build script detect changes in the React frontend?

The script emits cargo:rerun-if-changed=${DIST_DIR} for the entire dist/ directory path. Cargo monitors every file within this directory tree, triggering a rebuild of the llmfit-tui crate whenever any asset (HTML, JS, CSS) is modified, ensuring the embedded content stays synchronized with source changes.

What is the difference between include_str! and include_bytes! in this embedding pattern?

include_str! embeds the file contents as a &'static str, suitable for UTF-8 text files like HTML, CSS, and JavaScript, allowing direct string manipulation. include_bytes! embeds content as a &'static [u8], which is required for binary assets like images, fonts, or WebAssembly modules that may contain non-UTF-8 data. The llmfit-tui build script selects the appropriate macro based on the file extension.

How are the embedded assets served at runtime without a filesystem?

The generated generated_assets.rs module creates compile-time constants that point directly to the embedded data in the binary's read-only data section. The serve_api.rs HTTP handler matches incoming request paths to these constants and returns the static slices directly from memory, requiring no temporary files or disk access during execution.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →