zellij-org/zellij · error

duplicate top-level identifier '{}' declared in both '{}.js'

Error message

duplicate top-level identifier '{}' declared in both '{}.js' and '{}.js'

What it means

The asset bundler flattens every module in MODULE_ORDER into a single app.js by concatenating top-level declarations (column-0 function/const/let/var/class). If the same top-level identifier is declared in two module files, the concatenated script would contain a duplicate binding, which would be a runtime SyntaxError, so bundling aborts with the offending name and both modules.

Source

Thrown at xtask/src/assets.rs:132

    Ok(vec![
        (BUNDLE_FILE.to_string(), bundle),
        (INTEGRITY_FILE.to_string(), integrity),
        (INDEX_FILE.to_string(), index),
    ])
}

fn build_bundle(assets_dir: &Path) -> anyhow::Result<String> {
    let mut bundle = String::new();
    let mut declarations: BTreeMap<String, String> = BTreeMap::new();

    for module in MODULE_ORDER {
        let path = assets_dir.join(format!("{}.js", module));
        let source = std::fs::read_to_string(&path)
            .with_context(|| format!("failed to read '{}'", path.display()))?;
        let chunk = flatten_module(module, &source)?;
        for name in top_level_declarations(&chunk) {
            if let Some(previous) = declarations.insert(name.clone(), (*module).to_string()) {
                return Err(anyhow!(
                    "duplicate top-level identifier '{}' declared in both '{}.js' and '{}.js'",
                    name,
                    previous,
                    module
                ));
            }
        }
        bundle.push_str(&chunk);
        if !bundle.ends_with('\n') {
            bundle.push('\n');
        }
    }

    Ok(bundle)
}

fn flatten_module(module: &str, source: &str) -> anyhow::Result<String> {
    let mut out = String::new();

View on GitHub (pinned to 98a0837077)

Solutions

  1. Rename one of the duplicate top-level identifiers to something specific (e.g. `render` -> `renderTerminal`)
  2. If both modules need it, keep the declaration in one module and add `import { render } from './terminal.js';` in the other (imports are stripped by the flattener, so the single surviving declaration serves the whole bundle)
  3. Re-run `cargo xtask assets` to confirm the bundle builds

Example fix

// before: terminal.js and keyboard.js both declare top-level `render`
// keyboard.js
function render() { /* ... */ }

// after: keyboard.js imports it instead
import { render } from './terminal.js';
Defensive patterns

Strategy: validation

Validate before calling

# crude pre-flight: flag duplicated top-level declarations across bundled modules
for name in $(cat zellij-client/assets/*.js | grep -E '^(async function |function |const |let |var |class )' \
  | sed -E 's/^(async function |function |const |let |var |class )([A-Za-z0-9_$]+).*/\2/' | sort); do
  count=$(grep -cE "^(async function |function |const |let |var |class )${name}\\b" zellij-client/assets/*.js | awk -F: '{s+=$2} END {print s}')
  [ "$count" -gt 1 ] && echo "duplicate top-level identifier: $name"
done

Type guard

fn has_duplicate_top_level(module_a: &str, module_b: &str) -> bool {
    let names = |src: &str| -> std::collections::HashSet<String> {
        src.lines().filter(|l| !l.starts_with(char::is_whitespace))
            .filter_map(|l| ["function ", "const ", "let ", "var ", "class "]
                .iter().find_map(|p| l.strip_prefix(p)))
            .map(|r| r.chars().take_while(|c| c.is_alphanumeric() || *c == '_' || *c == '$').collect())
            .collect()
    };
    !names(module_a).is_disjoint(&names(module_b))
}

Try / catch

match assets::generate(&assets_dir) {
    Ok(_) => {}
    Err(e) if e.to_string().contains("duplicate top-level identifier") => {
        // extract the identifier from the message, rename it, regenerate
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Adding a top-level `function render(...)` (or const/let/var/class at column 0) in one module, e.g. keyboard.js, when `render` is already declared at top level in an earlier module such as terminal.js or utils.js.

Common situations: Copy-pasting helpers between modules; new modules using generic names like `init`, `handleEvent`, `config`; renaming a function in one file but not its twin in another.

Related errors


AI-assisted analysis of zellij-org/zellij@98a0837077 (2026-08-16). Data as JSON: /api/errors/63b5d6f5bcc32c26. Report an issue: GitHub.