fallow-core 2.79.0

Analysis orchestration for fallow codebase intelligence (dead code, duplication, plugins, cross-reference)
Documentation
//! Wrangler / Cloudflare Workers plugin.
//!
//! Detects Cloudflare Workers projects and marks worker entry points
//! and config files.

use std::path::Path;

use super::{Plugin, PluginResult, config_parser};

const ENABLERS: &[&str] = &["wrangler"];

const ENTRY_PATTERNS: &[&str] = &[
    "src/index.{ts,tsx,js,jsx,mts,mjs}",
    "src/worker.{ts,tsx,js,jsx,mts,mjs}",
    "functions/**/*.{ts,tsx,js,jsx,mts,mjs}",
];

const CONFIG_PATTERNS: &[&str] = &["wrangler.{toml,json,jsonc}"];

const ALWAYS_USED: &[&str] = &["wrangler.toml", "wrangler.json", "wrangler.jsonc"];

const TOOLING_DEPENDENCIES: &[&str] = &["wrangler", "@cloudflare/workers-types"];

define_plugin! {
    struct WranglerPlugin => "wrangler",
    enablers: ENABLERS,
    entry_patterns: ENTRY_PATTERNS,
    config_patterns: CONFIG_PATTERNS,
    always_used: ALWAYS_USED,
    tooling_dependencies: TOOLING_DEPENDENCIES,
    resolve_config(config_path, source, root) {
        let mut result = PluginResult::default();
        result.extend_entry_patterns(extract_main_entries(config_path, source, root));
        result
    },
}

fn extract_main_entries(config_path: &Path, source: &str, root: &Path) -> Vec<String> {
    let extension = config_path.extension().and_then(|ext| ext.to_str());
    let mut entries = match extension {
        Some("toml") => extract_toml_main_entries(config_path, source, root),
        _ => extract_js_main_entries(config_path, source, root),
    };
    entries.sort();
    entries.dedup();
    entries
}

fn extract_js_main_entries(config_path: &Path, source: &str, root: &Path) -> Vec<String> {
    let top_level = config_parser::extract_config_path_string(source, config_path, &["main"]);
    let env_entries = config_parser::extract_config_object_nested_strings(
        source,
        config_path,
        &["env"],
        &["main"],
    );
    top_level
        .into_iter()
        .chain(env_entries)
        .filter_map(|raw| config_parser::normalize_config_path(&raw, config_path, root))
        .collect()
}

fn extract_toml_main_entries(config_path: &Path, source: &str, root: &Path) -> Vec<String> {
    let Ok(value) = source.parse::<toml::Table>() else {
        return Vec::new();
    };

    let mut entries = Vec::new();
    if let Some(raw) = value.get("main").and_then(toml::Value::as_str)
        && let Some(path) = config_parser::normalize_config_path(raw, config_path, root)
    {
        entries.push(path);
    }

    if let Some(envs) = value.get("env").and_then(toml::Value::as_table) {
        for env in envs.values() {
            if let Some(raw) = env.get("main").and_then(toml::Value::as_str)
                && let Some(path) = config_parser::normalize_config_path(raw, config_path, root)
            {
                entries.push(path);
            }
        }
    }

    entries
}

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

    #[test]
    fn static_patterns_cover_worker_js_like_extensions() {
        let plugin = WranglerPlugin;

        assert!(
            plugin
                .entry_patterns()
                .contains(&"src/worker.{ts,tsx,js,jsx,mts,mjs}")
        );
    }

    #[test]
    fn jsonc_config_main_entries_are_entry_patterns() {
        let plugin = WranglerPlugin;
        let result = plugin.resolve_config(
            Path::new("/repo/apps/site/wrangler.jsonc"),
            r#"{
                // top-level worker
                "main": "src/worker.tsx",
                "env": {
                    "staging": { "main": "worker/entry.ts" }
                }
            }"#,
            Path::new("/repo"),
        );

        let entries: Vec<&str> = result
            .entry_patterns
            .iter()
            .map(|entry| entry.pattern.as_str())
            .collect();

        assert!(entries.contains(&"apps/site/src/worker.tsx"));
        assert!(entries.contains(&"apps/site/worker/entry.ts"));
    }

    #[test]
    fn toml_config_main_entries_are_entry_patterns() {
        let plugin = WranglerPlugin;
        let result = plugin.resolve_config(
            Path::new("/repo/wrangler.toml"),
            r#"
                name = "demo"
                main = "src/index.mts"

                [env.production]
                main = "src/production-worker.ts"
            "#,
            Path::new("/repo"),
        );

        let entries: Vec<&str> = result
            .entry_patterns
            .iter()
            .map(|entry| entry.pattern.as_str())
            .collect();

        assert!(
            entries.contains(&"src/index.mts"),
            "missing top-level main, entries={entries:?}"
        );
        assert!(
            entries.contains(&"src/production-worker.ts"),
            "missing env main, entries={entries:?}"
        );
    }
}