oxc-project/oxc · error · OxcDiagnostic

Unexpected assignment to `exports`.

Error message

Unexpected assignment to `exports`.

What it means

Diagnostic from oxlint rule node/exports-style, fired in BOTH modes by the shared helper is_global_exports_assignment_target: assigning to the `exports` binding itself is always wrong. In CommonJS `exports` is an alias created at module load; rebinding it (exports = { a: 1 }) exports nothing because require() returns module.exports, which still points at the original object. Unlike the two style diagnostics in this rule, this one catches a real bug and deserves prompt fixing.

Source

Thrown at crates/oxc_linter/src/rules/node/exports_style.rs:33

    context::LintContext,
    rule::{Rule, TupleRuleConfig},
    utils::{is_global_exports_assignment_target, is_global_module_exports},
};

fn unexpected_exports_diagnostic(span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("Unexpected access to `exports`.")
        .with_help("Use `module.exports` instead.")
        .with_label(span)
}

fn unexpected_module_exports_diagnostic(span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("Unexpected access to `module.exports`.")
        .with_help("Use `exports` instead.")
        .with_label(span)
}

fn unexpected_assignment_diagnostic(span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("Unexpected assignment to `exports`.")
        .with_help("Do not modify `exports` itself.")
        .with_label(span)
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
enum ExportsStyleMode {
    #[default]
    #[serde(rename = "module.exports")]
    /// Requires `module.exports` and disallows `exports`
    ModuleExports,
    /// Requires `exports` and disallows `module.exports`
    Exports,
}

#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
struct ExportsStyleOptions {

View on GitHub (pinned to e1e7af627c)

Solutions

  1. Assign to the real export target: 'module.exports = { a: 1 }'
  2. If you only meant to add to what is exported, mutate instead: 'exports.a = 1' (or module.exports.a = 1)
  3. Keep node/no-exports-assign or this rule enabled in CI to catch regressions — the bug is silent at runtime

Example fix

// before
exports = { hello: () => {} }; // require()rs get the ORIGINAL empty object

// after
module.exports = { hello: () => {} };
Defensive patterns

Strategy: validation

Validate before calling

// .oxlintrc.json — the assignment diagnostic runs in both modes; one of these suffices
"rules": { "node/no-exports-assign": "error" }

npx oxlint -c .oxlintrc.json --deny-warning .

Prevention

When it happens

Trigger: An assignment whose target resolves to the module-scope global `exports` — exports = { a: 1 };, exports = something || {}; — regardless of whether the rule's mode option is 'module.exports' or 'exports'. Property writes like exports.a = 1 do NOT trigger this diagnostic (those go to the access diagnostics per mode).

Common situations: Developers porting ES-module habits ('export default' feels like assigning the export); scaffolds that write exports = {}; beginners learning CommonJS aliasing; note the dedicated node/no-exports-assign rule reports the same bug with a different message, so enabling both yields two reports on one line.

Related errors


AI-assisted analysis of oxc-project/oxc@e1e7af627c (2026-08-20). Data as JSON: /api/errors/b40ff349553ed97a. Report an issue: GitHub.