diff --git a/src/ast/P.zig b/src/ast/P.zig index 5dbcb1b03b..3d912cb157 100644 --- a/src/ast/P.zig +++ b/src/ast/P.zig @@ -3787,6 +3787,169 @@ pub fn NewParser_( p.commonjs_named_exports_deoptimized = true; } + /// Attempts to recognize an `Object.defineProperty(exports, 'name', descriptor)` call + /// and convert it into an equivalent `exports.name = value` assignment expression. + /// Returns the rewritten expression if successful, or null if the pattern doesn't match. + /// + /// Recognized descriptor patterns: + /// - `{ value: expr }` or `{ value: expr, enumerable: true, ... }` + /// - `{ enumerable: true, get: function() { return expr; } }` + /// + /// Skipped cases: + /// - Export name is `"__esModule"` + /// - Descriptor contains a `set` property + /// - Descriptor has `enumerable: false` explicitly + pub fn tryExtractDefinePropertyExport(p: *P, e_: *E.Call, call_loc: logger.Loc) ?Expr { + // Must have exactly 3 arguments + if (e_.args.len != 3) return null; + + const args = e_.args.slice(); + + // Target must be Object.defineProperty + const dot = e_.target.data.as(.e_dot) orelse return null; + if (!strings.eqlComptime(dot.name, "defineProperty")) return null; + const dot_target_id = dot.target.data.as(.e_identifier) orelse return null; + const sym = p.symbols.items[dot_target_id.ref.innerIndex()]; + if (sym.kind != .unbound or !strings.eqlComptime(sym.original_name, "Object")) return null; + + // First arg must be `exports` ref or `module.exports` + // After visiting, `module.exports` may be rewritten to E.Special.module_exports + const first_arg = args[0]; + const is_module_exports = brk: { + if (first_arg.data == .e_identifier) { + if (first_arg.data.e_identifier.ref.eql(p.exports_ref)) break :brk false; + } + if (first_arg.data == .e_special and first_arg.data.e_special == .module_exports) { + break :brk true; + } + if (first_arg.data.as(.e_dot)) |fa_dot| { + if (fa_dot.target.data == .e_identifier and + fa_dot.target.data.e_identifier.ref.eql(p.module_ref) and + strings.eqlComptime(fa_dot.name, "exports")) + { + break :brk true; + } + } + return null; + }; + + // Second arg must be a string literal (the export name) + const name_str = args[1].data.as(.e_string) orelse return null; + if (!name_str.isUTF8()) return null; + const name = name_str.data; + + // Third arg must be an object literal (the descriptor) + const descriptor = args[2].data.as(.e_object) orelse return null; + + // Check for `set` property - if present, skip (not safe to convert) + // Also check for explicit `enumerable: false` + var has_enumerable_false = false; + for (descriptor.properties.slice()) |prop| { + const key = prop.key orelse continue; + const key_str = key.data.as(.e_string) orelse continue; + if (key_str.eqlComptime("set")) return null; + if (key_str.eqlComptime("enumerable")) { + if (prop.value) |val| { + if (val.data == .e_boolean and !val.data.e_boolean.value) { + has_enumerable_false = true; + } + } + } + } + if (has_enumerable_false) return null; + + // Try to extract the value from the descriptor + const export_value = p.extractDefinePropertyValue(descriptor) orelse return null; + + // Register the named export (same logic as maybe.zig for exports.X) + const named_export_entry = p.commonjs_named_exports.getOrPut(p.allocator, name) catch unreachable; + if (!named_export_entry.found_existing) { + const new_ref = p.newSymbol( + .other, + std.fmt.allocPrint(p.allocator, "${f}", .{bun.fmt.fmtIdentifier(name)}) catch unreachable, + ) catch unreachable; + bun.handleOom(p.module_scope.generated.append(p.allocator, new_ref)); + named_export_entry.value_ptr.* = .{ + .loc_ref = LocRef{ + .loc = args[1].loc, + .ref = new_ref, + }, + .needs_decl = true, + }; + if (p.commonjs_named_exports_needs_conversion == std.math.maxInt(u32)) + p.commonjs_named_exports_needs_conversion = @as(u32, @truncate(p.commonjs_named_exports.count() - 1)); + } + + const ref = named_export_entry.value_ptr.*.loc_ref.ref.?; + if (is_module_exports) { + p.recordUsage(ref); + } else { + p.ignoreUsage(p.exports_ref); + p.recordUsage(ref); + } + + // Rewrite as: exports.name = value (using E.Binary with E.CommonJSExportIdentifier) + const cjs_id = p.newExpr( + E.CommonJSExportIdentifier{ + .ref = ref, + .base = if (is_module_exports) .module_dot_exports else .exports, + }, + args[1].loc, + ); + + return p.newExpr( + E.Binary{ + .op = .bin_assign, + .left = cjs_id, + .right = export_value, + }, + call_loc, + ); + } + + /// Extracts the export value from a property descriptor object. + /// Handles two patterns: + /// - `{ value: expr }` -> returns expr + /// - `{ get: function() { return expr; } }` -> returns expr + fn extractDefinePropertyValue(p: *P, descriptor: *const E.Object) ?Expr { + _ = p; + var value_expr: ?Expr = null; + var get_expr: ?Expr = null; + + for (descriptor.properties.slice()) |prop| { + const key = prop.key orelse continue; + const key_str = key.data.as(.e_string) orelse continue; + + if (key_str.eqlComptime("value")) { + value_expr = prop.value; + } else if (key_str.eqlComptime("get")) { + get_expr = prop.value; + } + } + + // Prefer `value` pattern + if (value_expr) |val| { + return val; + } + + // Try `get` pattern: get: function() { return expr; } + if (get_expr) |get_val| { + const func = switch (get_val.data) { + .e_function => |f| f, + else => return null, + }; + // Must have no parameters + if (func.func.args.len != 0) return null; + // Body must be a single return statement + if (func.func.body.stmts.len != 1) return null; + const ret_stmt = func.func.body.stmts[0]; + if (ret_stmt.data != .s_return) return null; + return ret_stmt.data.s_return.value; + } + + return null; + } + pub fn maybeKeepExprSymbolName(p: *P, expr: Expr, original_name: string, was_anonymous_named_expr: bool) Expr { return if (was_anonymous_named_expr) p.keepExprSymbolName(expr, original_name) else expr; } diff --git a/src/ast/visitExpr.zig b/src/ast/visitExpr.zig index cc8bcb1b49..b605aa3cb9 100644 --- a/src/ast/visitExpr.zig +++ b/src/ast/visitExpr.zig @@ -1509,6 +1509,19 @@ pub fn VisitExpr( } }; + // Recognize Object.defineProperty(exports, 'name', { value: ... }) pattern + // generated by TypeScript/Babel transpilers and convert to exports.name = value + // to enable tree-shaking. + if (p.shouldUnwrapCommonJSToESM() and + !p.commonjs_named_exports_deoptimized and + !p.is_control_flow_dead and + p.current_scope == p.module_scope) + { + if (p.tryExtractDefinePropertyExport(e_, expr.loc)) |rewritten| { + return rewritten; + } + } + return expr; } pub fn e_new(p: *P, expr: Expr, _: ExprIn) Expr { diff --git a/test/bundler/bundler_cjs2esm.test.ts b/test/bundler/bundler_cjs2esm.test.ts index 0063ce5189..c3071746e8 100644 --- a/test/bundler/bundler_cjs2esm.test.ts +++ b/test/bundler/bundler_cjs2esm.test.ts @@ -392,4 +392,180 @@ describe("bundler", () => { stdout: '[[{"xyz":456},456],[{"xyz":123},123],[{"xyz":456},456],[{"xyz":123},123]]', }, }); + + // Object.defineProperty(exports, ...) pattern tests + + itBundled("cjs2esm/DefinePropertyValuePattern", { + files: { + "/entry.js": /* js */ ` + import { foo } from 'lib'; + console.log(foo); + `, + "/node_modules/lib/index.js": /* js */ ` + Object.defineProperty(exports, "__esModule", { value: true }); + Object.defineProperty(exports, "foo", { value: "hello" }); + `, + }, + cjs2esm: true, + run: { + stdout: "hello", + }, + }); + + itBundled("cjs2esm/DefinePropertyValueTreeShaking", { + files: { + "/entry.js": /* js */ ` + import { foo } from 'lib'; + console.log(foo); + `, + "/node_modules/lib/index.js": /* js */ ` + Object.defineProperty(exports, "__esModule", { value: true }); + Object.defineProperty(exports, "foo", { value: "kept" }); + Object.defineProperty(exports, "bar", { value: "remove_me" }); + `, + }, + cjs2esm: true, + dce: true, + treeShaking: true, + run: { + stdout: "kept", + }, + }); + + itBundled("cjs2esm/DefinePropertyGetterPattern", { + files: { + "/entry.js": /* js */ ` + import { pi } from 'lib'; + console.log(pi); + `, + "/node_modules/lib/index.js": /* js */ ` + Object.defineProperty(exports, "__esModule", { value: true }); + var _pi = 3.14; + Object.defineProperty(exports, "pi", { + enumerable: true, + get: function() { return _pi; } + }); + `, + }, + cjs2esm: true, + run: { + stdout: "3.14", + }, + }); + + itBundled("cjs2esm/DefinePropertyGetterTreeShaking", { + files: { + "/entry.js": /* js */ ` + import { used } from 'lib'; + console.log(used); + `, + "/node_modules/lib/index.js": /* js */ ` + Object.defineProperty(exports, "__esModule", { value: true }); + var _used = "yes"; + var _unused = "remove_me"; + Object.defineProperty(exports, "used", { + enumerable: true, + get: function() { return _used; } + }); + Object.defineProperty(exports, "unused", { + enumerable: true, + get: function() { return _unused; } + }); + `, + }, + cjs2esm: true, + dce: true, + treeShaking: true, + run: { + stdout: "yes", + }, + }); + + itBundled("cjs2esm/DefinePropertyMixedWithDirectExports", { + files: { + "/entry.js": /* js */ ` + import { foo, bar } from 'lib'; + console.log(foo, bar); + `, + "/node_modules/lib/index.js": /* js */ ` + Object.defineProperty(exports, "__esModule", { value: true }); + Object.defineProperty(exports, "foo", { value: "from_define" }); + exports.bar = "from_direct"; + `, + }, + cjs2esm: true, + run: { + stdout: "from_define from_direct", + }, + }); + + itBundled("cjs2esm/DefinePropertyModuleExports", { + files: { + "/entry.js": /* js */ ` + import { foo } from 'lib'; + console.log(foo); + `, + "/node_modules/lib/index.js": /* js */ ` + Object.defineProperty(module.exports, "__esModule", { value: true }); + Object.defineProperty(module.exports, "foo", { value: "via_module" }); + `, + }, + cjs2esm: true, + run: { + stdout: "via_module", + }, + }); + + itBundled("cjs2esm/DefinePropertyEnumerableFalseNotConverted", { + files: { + "/entry.js": /* js */ ` + const lib = require('lib'); + console.log(lib.foo); + `, + "/node_modules/lib/index.js": /* js */ ` + Object.defineProperty(exports, "foo", { enumerable: false, value: "hidden" }); + `, + }, + run: { + stdout: "hidden", + }, + }); + + itBundled("cjs2esm/DefinePropertyWithSetterNotConverted", { + files: { + "/entry.js": /* js */ ` + const lib = require('lib'); + console.log(lib.foo); + `, + "/node_modules/lib/index.js": /* js */ ` + var _foo = "with_setter"; + Object.defineProperty(exports, "foo", { + get: function() { return _foo; }, + set: function(v) { _foo = v; } + }); + `, + }, + run: { + stdout: "with_setter", + }, + }); + + itBundled("cjs2esm/DefinePropertyMultipleExports", { + files: { + "/entry.js": /* js */ ` + import { a, b, c } from 'lib'; + console.log(a, b, c); + `, + "/node_modules/lib/index.js": /* js */ ` + Object.defineProperty(exports, "__esModule", { value: true }); + Object.defineProperty(exports, "a", { value: 1 }); + Object.defineProperty(exports, "b", { enumerable: true, value: 2 }); + Object.defineProperty(exports, "c", { enumerable: true, get: function() { return 3; } }); + `, + }, + cjs2esm: true, + run: { + stdout: "1 2 3", + }, + }); });