chromewright 0.3.0

Browser automation MCP server via Chrome DevTools Protocol (CDP)
Documentation
use serde_json::Value;
use std::sync::OnceLock;

const BROWSER_KERNEL_JS: &str = include_str!("browser_kernel.js");

pub(crate) struct BrowserKernelTemplateShell {
    prefix: String,
    suffix: String,
}

impl BrowserKernelTemplateShell {
    fn compile(template: &'static str, config_placeholder: &'static str) -> Self {
        let expanded = template.replace("__BROWSER_KERNEL__", BROWSER_KERNEL_JS);
        let mut parts = expanded.split(config_placeholder);
        let prefix = parts
            .next()
            .expect("expanded browser-kernel template should have a prefix");
        let suffix = parts
            .next()
            .expect("browser-kernel template must contain exactly one config placeholder");
        assert!(
            parts.next().is_none(),
            "browser-kernel template must contain exactly one config placeholder"
        );

        Self {
            prefix: prefix.to_string(),
            suffix: suffix.to_string(),
        }
    }

    fn render(&self, config: &Value) -> String {
        let config_json = config.to_string();
        let mut rendered =
            String::with_capacity(self.prefix.len() + config_json.len() + self.suffix.len());
        rendered.push_str(&self.prefix);
        rendered.push_str(&config_json);
        rendered.push_str(&self.suffix);
        rendered
    }
}

pub(crate) fn render_browser_kernel_script(
    shell_cache: &OnceLock<BrowserKernelTemplateShell>,
    template: &'static str,
    config_placeholder: &'static str,
    config: &Value,
) -> String {
    shell_cache
        .get_or_init(|| BrowserKernelTemplateShell::compile(template, config_placeholder))
        .render(config)
}

#[cfg(test)]
mod tests {
    use super::BrowserKernelTemplateShell;
    use super::render_browser_kernel_script;
    use std::sync::OnceLock;

    #[test]
    fn test_render_browser_kernel_script_injects_kernel_and_config() {
        let shell = OnceLock::new();
        let rendered = render_browser_kernel_script(
            &shell,
            "const config = __CONFIG__;\n__BROWSER_KERNEL__\nreturn config;",
            "__CONFIG__",
            &serde_json::json!({ "selector": "#save" }),
        );

        assert!(rendered.contains(r##"const config = {"selector":"#save"};"##));
        assert!(rendered.contains("function resolveTargetMatch(config, options)"));
        assert!(!rendered.contains("__BROWSER_KERNEL__"));
        assert!(!rendered.contains("__CONFIG__"));
    }

    #[test]
    fn test_browser_kernel_template_shell_renders_equivalent_output() {
        let shell = BrowserKernelTemplateShell::compile(
            "const config = __CONFIG__;\n__BROWSER_KERNEL__\nreturn config.selector;",
            "__CONFIG__",
        );

        let rendered = shell.render(&serde_json::json!({ "selector": "#save" }));

        assert!(rendered.starts_with(r##"const config = {"selector":"#save"};"##));
        assert!(rendered.contains("function resolveTargetMatch(config, options)"));
        assert!(rendered.ends_with("\nreturn config.selector;"));
    }
}