no-mutable-exports
Disallow mutable let/var declarations on exported bindings
Keywords: mutable export, live binding, let, var, const, ESLint rule, JavaScript, TypeScript, auto-fix, LLM-optimized
Disallow export let and export var declarations — they create shared live bindings that all importers observe. This rule is part of eslint-plugin-modularity.
Quick Summary
| Aspect | Details |
|---|---|
| Severity | Warning (code quality) |
| Auto-Fix | ✅ Yes (replaces let/var with const) |
| Category | Modularity |
| ESLint MCP | ✅ Optimized for ESLint MCP integration |
| Best For | All JavaScript/TypeScript codebases with ES module exports |
Rule Details
ES modules export live bindings. When you write export let count = 0, every importer gets a reference to the same binding — if the exporting module reassigns count, all importers immediately see the new value. This creates invisible coupling across module boundaries and makes behavior hard to reason about.
The auto-fix replaces let or var with const. If the variable is later reassigned inside the module you will need to refactor that logic (e.g. encapsulate mutation behind an exported function), but the fix surfaces the issue immediately.
export const, export function, and export class are all fine and are never flagged.
Why This Matters
| Issue | Impact | Solution |
|---|---|---|
| 🔗 Implicit coupling | All importers share the same mutable binding | Use const or encapsulate state |
| 🐛 Spooky action | Remote reassignment surprises importers | Export functions instead |
| 🔄 Predictability | Module interface becomes stateful | Keep exports immutable |
Examples
❌ Incorrect
// Mutable export creates a live binding
export let count = 0;
export var config = {};✅ Correct
// Immutable exports
export const count = 0;
export const config = Object.freeze({});
export function increment() { return count + 1; }
export class Counter {}Auto-fix
ESLint can fix this automatically. Running eslint --fix replaces the mutable keyword with const.
Before
export let count = 0;
export var config = {};After
export const count = 0;
export const config = {};If the variable is reassigned inside the module, TypeScript or the runtime will surface the error — which is exactly the intent. Refactor the mutation behind an exported function rather than using a mutable live binding.
Configuration Examples
Basic Usage
{
rules: {
'modularity/no-mutable-exports': 'warn'
}
}Related Rules
no-external-api-calls-in-utils- Keep utility modules side-effect freeenforce-naming- Consistent naming across module boundaries
Further Reading
- export - MDN - MDN reference on live bindings
- ES Modules in Depth - ECMAScript specification
Known False Negatives
The following patterns are not detected due to static analysis limitations:
Re-exported Mutable Bindings
Why: When a mutable binding is imported from another module and then re-exported, the rule cannot trace the original declaration.
// ❌ NOT DETECTED - re-export of a mutable binding
import { count } from './state';
export { count }; // still a live binding if count was declared with letMitigation: Apply the rule in the originating module and review re-export chains manually.
Namespace Exports
Why: export * from './module' bulk-re-exports all bindings; the rule cannot inspect the source module statically.
// ❌ NOT DETECTED - bulk re-export
export * from './mutable-state';Mitigation: Avoid export * for modules that contain mutable state; prefer explicit named exports.