Skip to content

[Bug]: Optional chaining (?.) stripped when rewriting namespace member expressions #8710

@Dunqing

Description

@Dunqing

Reproduction link or steps

// state.js
export const app = { user: null };

// main.js
import * as h from './state.js';
console.log(h.app?.user?.name ?? 'ok');

Bundle with: rolldown main.js --dir dist

What is expected?

The optional chaining should be preserved:

console.log({ user: null }?.user?.name ?? "ok");
// or after inlining the object:
// the ?.user access should still be guarded

The expression should safely evaluate to "ok".

What is actually happening?

The output is:

console.log({ user: null }.user.name ?? "ok");

All ?. operators are stripped, causing TypeError: Cannot read properties of null (reading 'name') at runtime.

Root cause analysis

The bug is in the module finalizer's handling of ChainExpression during namespace member expression rewriting:

1. crates/rolldown/src/module_finalizers/impl_visit_mut.rs (lines ~485-492)

ast::Expression::ChainExpression(chain_expr) => {
    if let Some(new_expr) = chain_expr
        .expression
        .as_member_expression_mut()
        .and_then(|expr| self.try_rewrite_member_expr(expr))
    {
        *expr = new_expr;  // replaces entire ChainExpression, dropping ?. semantics
    }
}

When a ChainExpression wraps a member expression that gets rewritten (namespace import resolution), the result replaces the entire ChainExpression — dropping the optional chaining wrapper.

2. crates/rolldown_ecmascript_utils/src/ast_snippet.rs (member_expr_or_ident_ref)

ast::Expression::from(self.builder.member_expression_static(
    SPAN, cur, self.id_name(name, *related_span),
    false,  // always non-optional
))

member_expr_or_ident_ref always creates member expressions with optional: false, never preserving the original optional flags.

Impact

This is a correctness bug that silently changes runtime behavior. It affects any code using optional chaining on namespace imports or their resolved bindings. Reported in vitejs/vite#21862 as a Vite 8 production build issue.

Any additional comments?

Tested with rolldown v1.0.0-rc.9. The oxc parser, minifier, transformer, and codegen all handle optional chaining correctly in isolation — this is purely a rolldown module finalizer issue.

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions