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
- Rename one of the duplicate top-level identifiers to something specific (e.g. `render` -> `renderTerminal`)
- 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)
- 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
- Namespace identifiers per module (terminalRender, keyboardInit) instead of generic names
- Import shared helpers from the module that owns them rather than redeclaring them
- Run `cargo xtask assets` locally before pushing; CI duplicates check is slower feedback
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
- dynamic import is not supported at {}
- unrecognised import syntax at {}
- unterminated import statement at {}
- unrecognised export syntax at {}
- unsupported export form at {}
AI-assisted analysis of zellij-org/zellij@98a0837077 (2026-08-16).
Data as JSON: /api/errors/63b5d6f5bcc32c26.
Report an issue: GitHub.