sinelaw/fresh · error

Plugin dependency cycle detected among

Error message

Plugin dependency cycle detected among: {}. These plugins will not be loaded.

What it means

After Kahn's algorithm, any plugin not placed into the result must be part of (or reachable only through) a dependency cycle; `topological_sort_plugins` reports those plugin names and refuses to load any of them, since load order cannot be established.

Solutions

  1. Break the cycle among the listed plugins by removing one dependency edge in config/plugin metadata.
  2. Remove one of the mutually-dependent plugins if the dependency is unnecessary.
  3. If self-dependency, drop the plugin's reference to itself.

Example fix

// before
{name = "a", deps = ["b"]}
{name = "b", deps = ["a"]}
// after
{name = "a", deps = ["b"]}
{name = "b", deps = []}
Defensive patterns

Strategy: validation

Validate before calling

// Detect cycles before sorting (simple DFS)
fn has_cycle(plugins: &[Plugin]) -> bool {
    fn visit(n: &str, deps: &HashMap<&str, Vec<&str>>, seen: &mut HashSet<&str>, stack: &mut HashSet<&str>) -> bool {
        if stack.contains(n) { return true; }
        if !seen.insert(n) { return false; }
        stack.insert(n);
        deps.get(n).map_or(false, |ds| ds.iter().any(|d| visit(d, deps, seen, stack)))
            || { stack.remove(n); false }
    }
    let deps: HashMap<&str, Vec<&str>> = plugins.iter().map(|p| (p.name.as_str(), p.deps.iter().map(|s| s.as_str()).collect())).collect();
    let mut seen = HashSet::new();
    plugins.iter().any(|p| visit(&p.name, &deps, &mut seen, &mut HashSet::new()))
}

Try / catch

match topological_sort_plugins(&plugins) {
    Err(e) if e.to_string().contains("dependency cycle") => {
        log::error!("cycle: {e}");
        Vec::new() // load none, keep editor running
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling `topological_sort_plugins` where plugin dependencies form a cycle (A→B→A, or a self-dependency A→A), detected by leftover nodes with nonzero in-degree.

Common situations: Two plugins each listing the other as a dependency after config edits; a plugin accidentally depending on itself; merging configs that introduced a circular chain.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of sinelaw/fresh@67894ca546 (2026-09-13). Data as JSON: /api/errors/2899da54693e4a66. Report an issue: GitHub.

Appendix: source

Thrown at crates/fresh-parser-js/src/lib.rs:273

                    }
                }
            }
            // Sort newly ready plugins alphabetically for determinism
            newly_ready.sort();
            queue.extend(newly_ready);
            queue.sort(); // maintain overall alphabetical order among ready nodes
        }
    }

    if result.len() != plugin_names.len() {
        // Some plugins are in a cycle — find them
        let in_result: HashSet<&str> = result.iter().map(|s| s.as_str()).collect();
        let cycle_plugins: Vec<String> = plugin_names
            .iter()
            .filter(|n| !in_result.contains(n.as_str()))
            .cloned()
            .collect();
        return Err(anyhow!(
            "Plugin dependency cycle detected among: {}. These plugins will not be loaded.",
            cycle_plugins.join(", ")
        ));
    }

    Ok(result)
}

/// Module metadata for scoped bundling
#[derive(Debug, Clone)]
struct ModuleMetadata {
    /// Canonical path to this module
    path: PathBuf,
    /// Variable name for this module's exports (e.g., "__mod_panel_manager")
    var_name: String,
    /// Named imports from other modules
    imports: Vec<ImportBinding>,
    /// Named exports from this module

View on GitHub (pinned to 67894ca546)