diff --git a/docs/bundler/index.md b/docs/bundler/index.md index 21bb749f04..4680d8cc5a 100644 --- a/docs/bundler/index.md +++ b/docs/bundler/index.md @@ -1130,6 +1130,26 @@ $ bun build ./index.tsx --outdir ./out --footer="// built with love in SF" {% /codetabs %} +### `drop` + +Remove function calls from a bundle. For example, `--drop=console` will remove all calls to `console.log`. Arguments to calls will also be removed, regardless of if those arguments may have side effects. Dropping `debugger` will remove all `debugger` statements. + +{% codetabs %} + +```ts#JavaScript +await Bun.build({ + entrypoints: ['./index.tsx'], + outdir: './out', + drop: ["console", "debugger", "anyIdentifier.or.propertyAccess"], +}) +``` + +```bash#CLI +$ bun build ./index.tsx --outdir ./out --drop=console --drop=debugger --drop=anyIdentifier.or.propertyAccess +``` + +{% /codetabs %} + ### `experimentalCss` Whether to enable _experimental_ support for bundling CSS files. Defaults to `false`. diff --git a/docs/bundler/vs-esbuild.md b/docs/bundler/vs-esbuild.md index 8a42354da5..1266914c05 100644 --- a/docs/bundler/vs-esbuild.md +++ b/docs/bundler/vs-esbuild.md @@ -190,8 +190,7 @@ In Bun's CLI, simple boolean flags like `--minify` do not accept an argument. Ot --- - `--drop` -- n/a -- Not supported +- `--drop` --- diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 209efa034e..f1b51b96a1 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -1612,6 +1612,11 @@ declare module "bun" { * Enable CSS support. */ experimentalCss?: boolean; + + /** + * Drop function calls to matching property accesses. + */ + drop?: string[]; } namespace Password { diff --git a/src/api/schema.zig b/src/api/schema.zig index 1c3679be8d..bec43fbde7 100644 --- a/src/api/schema.zig +++ b/src/api/schema.zig @@ -1635,6 +1635,8 @@ pub const Api = struct { /// define define: ?StringMap = null, + drop: []const []const u8 = &.{}, + /// preserve_symlinks preserve_symlinks: ?bool = null, diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index 84d6f8208e..e9397f692b 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -590,7 +590,7 @@ pub fn inspect( // we are going to always clone to keep things simple for now // the common case here will be stack-allocated, so it should be fine - var out = ZigString.init(array.toOwnedSliceLeaky()).withEncoding(); + var out = ZigString.init(array.slice()).withEncoding(); const ret = out.toJS(globalThis); array.deinit(); return ret; @@ -3932,7 +3932,7 @@ const TOMLObject = struct { return .zero; }; - const slice = writer.ctx.buffer.toOwnedSliceLeaky(); + const slice = writer.ctx.buffer.slice(); var out = bun.String.fromUTF8(slice); defer out.deref(); diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index 72a4b0ea6f..5e52a877e3 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -75,6 +75,7 @@ pub const JSBundler = struct { banner: OwnedString = OwnedString.initEmpty(bun.default_allocator), footer: OwnedString = OwnedString.initEmpty(bun.default_allocator), experimental_css: bool = false, + drop: bun.StringSet = bun.StringSet.init(bun.default_allocator), pub const List = bun.StringArrayHashMapUnmanaged(Config); @@ -191,7 +192,6 @@ pub const JSBundler = struct { try this.banner.appendSliceExact(slice.slice()); } - if (try config.getOptional(globalThis, "footer", ZigString.Slice)) |slice| { defer slice.deinit(); try this.footer.appendSliceExact(slice.slice()); @@ -351,6 +351,18 @@ pub const JSBundler = struct { } } + if (try config.getOwnArray(globalThis, "drop")) |drops| { + var iter = drops.arrayIterator(globalThis); + while (iter.next()) |entry| { + var slice = entry.toSliceOrNull(globalThis) orelse { + globalThis.throwInvalidArguments("Expected drop to be an array of strings", .{}); + return error.JSError; + }; + defer slice.deinit(); + try this.drop.insert(slice.slice()); + } + } + // if (try config.getOptional(globalThis, "dir", ZigString.Slice)) |slice| { // defer slice.deinit(); // this.appendSliceExact(slice.slice()) catch unreachable; @@ -544,6 +556,9 @@ pub const JSBundler = struct { self.rootdir.deinit(); self.public_path.deinit(); self.conditions.deinit(); + self.drop.deinit(); + self.banner.deinit(); + self.footer.deinit(); } }; diff --git a/src/bun.js/api/html_rewriter.zig b/src/bun.js/api/html_rewriter.zig index ef86b5083f..767662975c 100644 --- a/src/bun.js/api/html_rewriter.zig +++ b/src/bun.js/api/html_rewriter.zig @@ -710,7 +710,7 @@ pub const HTMLRewriter = struct { // pub fn done(this: *StreamOutputSink) void { // var prev_value = this.response.body.value; - // var bytes = this.bytes.toOwnedSliceLeaky(); + // var bytes = this.bytes.slice(); // this.response.body.value = .{ // .Blob = JSC.WebCore.Blob.init(bytes, this.bytes.allocator, this.global), // }; diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 75b755bce1..2fc5a560b0 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -3129,7 +3129,7 @@ pub const JSGlobalObject = opaque { return ZigString.static(fmt).toErrorInstance(this); // Ensure we clone it. - var str = ZigString.initUTF8(buf.toOwnedSliceLeaky()); + var str = ZigString.initUTF8(buf.slice()); return str.toErrorInstance(this); } else { @@ -3148,7 +3148,7 @@ pub const JSGlobalObject = opaque { defer buf.deinit(); var writer = buf.writer(); writer.print(fmt, args) catch return ZigString.static(fmt).toErrorInstance(this); - var str = ZigString.fromUTF8(buf.toOwnedSliceLeaky()); + var str = ZigString.fromUTF8(buf.slice()); return str.toTypeErrorInstance(this); } else { return ZigString.static(fmt).toTypeErrorInstance(this); @@ -3162,7 +3162,7 @@ pub const JSGlobalObject = opaque { defer buf.deinit(); var writer = buf.writer(); writer.print(fmt, args) catch return ZigString.static(fmt).toErrorInstance(this); - var str = ZigString.fromUTF8(buf.toOwnedSliceLeaky()); + var str = ZigString.fromUTF8(buf.slice()); return str.toSyntaxErrorInstance(this); } else { return ZigString.static(fmt).toSyntaxErrorInstance(this); @@ -3176,7 +3176,7 @@ pub const JSGlobalObject = opaque { defer buf.deinit(); var writer = buf.writer(); writer.print(fmt, args) catch return ZigString.static(fmt).toErrorInstance(this); - var str = ZigString.fromUTF8(buf.toOwnedSliceLeaky()); + var str = ZigString.fromUTF8(buf.slice()); return str.toRangeErrorInstance(this); } else { return ZigString.static(fmt).toRangeErrorInstance(this); @@ -4619,7 +4619,7 @@ pub const JSValue = enum(JSValueReprInt) { var writer = buf.writer(); try writer.print(fmt, args); - return String.init(buf.toOwnedSliceLeaky()).toJS(globalThis); + return String.init(buf.slice()).toJS(globalThis); } /// Create a JSValue string from a zig format-print (fmt + args), with pretty format @@ -4633,7 +4633,7 @@ pub const JSValue = enum(JSValueReprInt) { switch (Output.enable_ansi_colors) { inline else => |enabled| try writer.print(Output.prettyFmt(fmt, enabled), args), } - return String.init(buf.toOwnedSliceLeaky()).toJS(globalThis); + return String.init(buf.slice()).toJS(globalThis); } pub fn fromEntries(globalThis: *JSGlobalObject, keys_array: [*c]ZigString, values_array: [*c]ZigString, strings_count: usize, clone: bool) JSValue { diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index f597d2dbd7..edab5fba41 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -2143,7 +2143,7 @@ pub const ModuleLoader = struct { writer.writeAll(";\n") catch bun.outOfMemory(); } - const public_url = bun.String.createUTF8(buf.toOwnedSliceLeaky()); + const public_url = bun.String.createUTF8(buf.slice()); return ResolvedSource{ .allocator = &jsc_vm.allocator, .source_code = public_url, diff --git a/src/bun.js/test/diff_format.zig b/src/bun.js/test/diff_format.zig index c907d16fd4..fc04a74e33 100644 --- a/src/bun.js/test/diff_format.zig +++ b/src/bun.js/test/diff_format.zig @@ -129,8 +129,8 @@ pub const DiffFormatter = struct { buffered_writer.flush() catch unreachable; } - const received_slice = received_buf.toOwnedSliceLeaky(); - const expected_slice = expected_buf.toOwnedSliceLeaky(); + const received_slice = received_buf.slice(); + const expected_slice = expected_buf.slice(); if (this.not) { const not_fmt = "Expected: not {s}"; diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index b522dc8cfb..35a417ad4f 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -2757,7 +2757,7 @@ pub const Expect = struct { }; defer pretty_value.deinit(); - if (strings.eqlLong(pretty_value.toOwnedSliceLeaky(), saved_value, true)) { + if (strings.eqlLong(pretty_value.slice(), saved_value, true)) { Jest.runner.?.snapshots.passed += 1; return .undefined; } @@ -2766,7 +2766,7 @@ pub const Expect = struct { const signature = comptime getSignature("toMatchSnapshot", "expected", false); const fmt = signature ++ "\n\n{any}\n"; const diff_format = DiffFormatter{ - .received_string = pretty_value.toOwnedSliceLeaky(), + .received_string = pretty_value.slice(), .expected_string = saved_value, .globalThis = globalThis, }; @@ -5443,7 +5443,7 @@ pub const ExpectCustomAsymmetricMatcher = struct { return .zero; }; if (printed) { - return bun.String.init(mutable_string.toOwnedSliceLeaky()).toJS(); + return bun.String.init(mutable_string.slice()).toJS(); } return ExpectMatcherUtils.printValue(globalThis, this, null); } diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index a7d370f35e..ace7a2172a 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -1788,7 +1788,7 @@ inline fn createScope( buffer.reset(); appendParentLabel(&buffer, parent) catch @panic("Bun ran out of memory while filtering tests"); buffer.append(label) catch unreachable; - const str = bun.String.fromBytes(buffer.toOwnedSliceLeaky()); + const str = bun.String.fromBytes(buffer.slice()); is_skip = !regex.matches(str); if (is_skip) { tag_to_use = .skip; @@ -2087,7 +2087,7 @@ fn eachBind( buffer.reset(); appendParentLabel(&buffer, parent) catch @panic("Bun ran out of memory while filtering tests"); buffer.append(formattedLabel) catch unreachable; - const str = bun.String.fromBytes(buffer.toOwnedSliceLeaky()); + const str = bun.String.fromBytes(buffer.slice()); is_skip = !regex.matches(str); } diff --git a/src/bun.js/web_worker.zig b/src/bun.js/web_worker.zig index 7f64f84842..848b84dfde 100644 --- a/src/bun.js/web_worker.zig +++ b/src/bun.js/web_worker.zig @@ -331,7 +331,7 @@ pub const WebWorker = struct { bun.outOfMemory(); }; JSC.markBinding(@src()); - WebWorker__dispatchError(globalObject, worker.cpp_worker, bun.String.createUTF8(array.toOwnedSliceLeaky()), error_instance); + WebWorker__dispatchError(globalObject, worker.cpp_worker, bun.String.createUTF8(array.slice()), error_instance); if (vm.worker) |worker_| { _ = worker.setRequestedTerminate(); worker.parent_poll_ref.unrefConcurrently(worker.parent); diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 3cc46fbe3a..450086c12c 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -1439,7 +1439,7 @@ pub const BundleV2 = struct { .entry_points = config.entry_points.keys(), .target = config.target.toAPI(), .absolute_working_dir = if (config.dir.list.items.len > 0) - config.dir.toOwnedSliceLeaky() + config.dir.slice() else null, .inject = &.{}, @@ -1449,6 +1449,7 @@ pub const BundleV2 = struct { .env_files = &.{}, .conditions = config.conditions.map.keys(), .ignore_dce_annotations = bundler.options.ignore_dce_annotations, + .drop = config.drop.map.keys(), }, completion.env, ); @@ -1466,8 +1467,8 @@ pub const BundleV2 = struct { bundler.options.output_format = config.format; bundler.options.bytecode = config.bytecode; - bundler.options.output_dir = config.outdir.toOwnedSliceLeaky(); - bundler.options.root_dir = config.rootdir.toOwnedSliceLeaky(); + bundler.options.output_dir = config.outdir.slice(); + bundler.options.root_dir = config.rootdir.slice(); bundler.options.minify_syntax = config.minify.syntax; bundler.options.minify_whitespace = config.minify.whitespace; bundler.options.minify_identifiers = config.minify.identifiers; @@ -1478,8 +1479,8 @@ pub const BundleV2 = struct { bundler.options.emit_dce_annotations = config.emit_dce_annotations orelse !config.minify.whitespace; bundler.options.ignore_dce_annotations = config.ignore_dce_annotations; bundler.options.experimental_css = config.experimental_css; - bundler.options.banner = config.banner.toOwnedSlice(); - bundler.options.footer = config.footer.toOwnedSlice(); + bundler.options.banner = config.banner.slice(); + bundler.options.footer = config.footer.slice(); bundler.configureLinker(); try bundler.configureDefines(); @@ -1545,7 +1546,7 @@ pub const BundleV2 = struct { bun.default_allocator.dupe( u8, bun.path.joinAbsString( - this.config.outdir.toOwnedSliceLeaky(), + this.config.outdir.slice(), &[_]string{output_file.dest_path}, .auto, ), @@ -1555,7 +1556,7 @@ pub const BundleV2 = struct { u8, bun.path.joinAbsString( Fs.FileSystem.instance.top_level_dir, - &[_]string{ this.config.dir.toOwnedSliceLeaky(), this.config.outdir.toOwnedSliceLeaky(), output_file.dest_path }, + &[_]string{ this.config.dir.slice(), this.config.outdir.slice(), output_file.dest_path }, .auto, ), ) catch unreachable @@ -8950,7 +8951,7 @@ pub const LinkerContext = struct { const input = c.parse_graph.input_files.items(.source)[chunk.entry_point.source_index].path; var buf = MutableString.initEmpty(worker.allocator); js_printer.quoteForJSONBuffer(input.pretty, &buf, true) catch bun.outOfMemory(); - const str = buf.toOwnedSliceLeaky(); // worker.allocator is an arena + const str = buf.slice(); // worker.allocator is an arena j.pushStatic(str); line_offset.advance(str); } diff --git a/src/cli.zig b/src/cli.zig index 61141fe939..ada7cbe4a9 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -194,10 +194,11 @@ pub const Arguments = struct { }; const transpiler_params_ = [_]ParamType{ - clap.parseParam("--main-fields ... Main fields to lookup in package.json. Defaults to --target dependent") catch unreachable, + clap.parseParam("--main-fields ... Main fields to lookup in package.json. Defaults to --target dependent") catch unreachable, clap.parseParam("--extension-order ... Defaults to: .tsx,.ts,.jsx,.js,.json ") catch unreachable, - clap.parseParam("--tsconfig-override Specify custom tsconfig.json. Default $cwd/tsconfig.json") catch unreachable, - clap.parseParam("-d, --define ... Substitute K:V while parsing, e.g. --define process.env.NODE_ENV:\"development\". Values are parsed as JSON.") catch unreachable, + clap.parseParam("--tsconfig-override Specify custom tsconfig.json. Default $cwd/tsconfig.json") catch unreachable, + clap.parseParam("-d, --define ... Substitute K:V while parsing, e.g. --define process.env.NODE_ENV:\"development\". Values are parsed as JSON.") catch unreachable, + clap.parseParam("--drop ... Remove function calls, e.g. --drop=console removes all console.* calls.") catch unreachable, clap.parseParam("-l, --loader ... Parse files with .ext:loader, e.g. --loader .js:jsx. Valid loaders: js, jsx, ts, tsx, json, toml, text, file, wasm, napi") catch unreachable, clap.parseParam("--no-macros Disable macros from being executed in the bundler, transpiler and runtime") catch unreachable, clap.parseParam("--jsx-factory Changes the function called when compiling JSX elements using the classic JSX runtime") catch unreachable, @@ -590,6 +591,8 @@ pub const Arguments = struct { }; } + opts.drop = args.options("--drop"); + const loader_tuple = try LoaderColonList.resolve(allocator, args.options("--loader")); if (loader_tuple.keys.len > 0) { diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig index d973ef5b75..3c0e9c6e29 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -99,6 +99,7 @@ pub const BuildCommand = struct { this_bundler.options.banner = ctx.bundler_options.banner; this_bundler.options.footer = ctx.bundler_options.footer; + this_bundler.options.drop = ctx.args.drop; this_bundler.options.experimental_css = ctx.bundler_options.experimental_css; @@ -236,10 +237,11 @@ pub const BuildCommand = struct { allocator, user_defines.keys, user_defines.values, - ), log, allocator) + ), ctx.args.drop, log, allocator) else null, null, + this_bundler.options.define.drop_debugger, ); try bun.bake.addImportMetaDefines(allocator, this_bundler.options.define, .development, .server); diff --git a/src/cli/upgrade_command.zig b/src/cli/upgrade_command.zig index b89d1777ad..8cdb5665c6 100644 --- a/src/cli/upgrade_command.zig +++ b/src/cli/upgrade_command.zig @@ -559,7 +559,7 @@ pub const UpgradeCommand = struct { else => return error.HTTPError, } - const bytes = zip_file_buffer.toOwnedSliceLeaky(); + const bytes = zip_file_buffer.slice(); progress.end(); refresher.refresh(); diff --git a/src/defines.zig b/src/defines.zig index 39495728af..0cd0b427de 100644 --- a/src/defines.zig +++ b/src/defines.zig @@ -53,6 +53,8 @@ pub const DefineData = struct { // have any observable side effects. call_can_be_unwrapped_if_unused: bool = false, + method_call_must_be_replaced_with_undefined: bool = false, + pub fn isUndefined(self: *const DefineData) bool { return self.valueless; } @@ -70,75 +72,87 @@ pub const DefineData = struct { .can_be_removed_if_unused = a.can_be_removed_if_unused, .call_can_be_unwrapped_if_unused = a.call_can_be_unwrapped_if_unused, .original_name = b.original_name, + .valueless = a.method_call_must_be_replaced_with_undefined or b.method_call_must_be_replaced_with_undefined, + .method_call_must_be_replaced_with_undefined = a.method_call_must_be_replaced_with_undefined or b.method_call_must_be_replaced_with_undefined, }; } - pub fn fromMergeableInput(defines: RawDefines, user_defines: *UserDefines, log: *logger.Log, allocator: std.mem.Allocator) !void { - try user_defines.ensureUnusedCapacity(@truncate(defines.count())); - var iter = defines.iterator(); - while (iter.next()) |entry| { - var keySplitter = std.mem.split(u8, entry.key_ptr.*, "."); - while (keySplitter.next()) |part| { - if (!js_lexer.isIdentifier(part)) { - if (strings.eql(part, entry.key_ptr)) { - try log.addErrorFmt(null, logger.Loc{}, allocator, "define key \"{s}\" must be a valid identifier", .{entry.key_ptr.*}); - } else { - try log.addErrorFmt(null, logger.Loc{}, allocator, "define key \"{s}\" contains invalid identifier \"{s}\"", .{ part, entry.value_ptr.* }); - } - break; + pub fn fromMergeableInputEntry(user_defines: *UserDefines, key: []const u8, value_str: []const u8, value_is_undefined: bool, method_call_must_be_replaced_with_undefined: bool, log: *logger.Log, allocator: std.mem.Allocator) !void { + var keySplitter = std.mem.split(u8, key, "."); + while (keySplitter.next()) |part| { + if (!js_lexer.isIdentifier(part)) { + if (strings.eql(part, key)) { + try log.addErrorFmt(null, logger.Loc{}, allocator, "define key \"{s}\" must be a valid identifier", .{key}); + } else { + try log.addErrorFmt(null, logger.Loc{}, allocator, "define key \"{s}\" contains invalid identifier \"{s}\"", .{ part, value_str }); } + break; } - - // check for nested identifiers - var valueSplitter = std.mem.split(u8, entry.value_ptr.*, "."); - var isIdent = true; - - while (valueSplitter.next()) |part| { - if (!js_lexer.isIdentifier(part) or js_lexer.Keywords.has(part)) { - isIdent = false; - break; - } - } - - if (isIdent) { - // Special-case undefined. it's not an identifier here - // https://github.com/evanw/esbuild/issues/1407 - const value = if (strings.eqlComptime(entry.value_ptr.*, "undefined")) - js_ast.Expr.Data{ .e_undefined = js_ast.E.Undefined{} } - else - js_ast.Expr.Data{ .e_identifier = .{ - .ref = Ref.None, - .can_be_removed_if_unused = true, - } }; - - user_defines.putAssumeCapacity( - entry.key_ptr.*, - DefineData{ - .value = value, - .original_name = entry.value_ptr.*, - .can_be_removed_if_unused = true, - }, - ); - continue; - } - const _log = log; - var source = logger.Source{ - .contents = entry.value_ptr.*, - .path = defines_path, - .key_path = fs.Path.initWithNamespace("defines", "internal"), - }; - const expr = try json_parser.parseEnvJSON(&source, _log, allocator); - const cloned = try expr.data.deepClone(allocator); - user_defines.putAssumeCapacity(entry.key_ptr.*, DefineData{ - .value = cloned, - .can_be_removed_if_unused = expr.isPrimitiveLiteral(), - }); } + + // check for nested identifiers + var valueSplitter = std.mem.split(u8, value_str, "."); + var isIdent = true; + + while (valueSplitter.next()) |part| { + if (!js_lexer.isIdentifier(part) or js_lexer.Keywords.has(part)) { + isIdent = false; + break; + } + } + + if (isIdent) { + // Special-case undefined. it's not an identifier here + // https://github.com/evanw/esbuild/issues/1407 + const value = if (value_is_undefined or strings.eqlComptime(value_str, "undefined")) + js_ast.Expr.Data{ .e_undefined = js_ast.E.Undefined{} } + else + js_ast.Expr.Data{ .e_identifier = .{ + .ref = Ref.None, + .can_be_removed_if_unused = true, + } }; + + user_defines.putAssumeCapacity( + key, + DefineData{ + .value = value, + .original_name = value_str, + .can_be_removed_if_unused = true, + .valueless = value_is_undefined, + .method_call_must_be_replaced_with_undefined = method_call_must_be_replaced_with_undefined, + }, + ); + return; + } + const _log = log; + var source = logger.Source{ + .contents = value_str, + .path = defines_path, + .key_path = fs.Path.initWithNamespace("defines", "internal"), + }; + const expr = try json_parser.parseEnvJSON(&source, _log, allocator); + const cloned = try expr.data.deepClone(allocator); + user_defines.putAssumeCapacity(key, DefineData{ + .value = cloned, + .can_be_removed_if_unused = expr.isPrimitiveLiteral(), + .valueless = value_is_undefined, + .method_call_must_be_replaced_with_undefined = method_call_must_be_replaced_with_undefined, + }); } - pub fn fromInput(defines: RawDefines, log: *logger.Log, allocator: std.mem.Allocator) !UserDefines { + pub fn fromInput(defines: RawDefines, drop: []const []const u8, log: *logger.Log, allocator: std.mem.Allocator) !UserDefines { var user_defines = UserDefines.init(allocator); - try fromMergeableInput(defines, &user_defines, log, allocator); + var iterator = defines.iterator(); + try user_defines.ensureUnusedCapacity(@truncate(defines.count() + drop.len)); + while (iterator.next()) |entry| { + try fromMergeableInputEntry(&user_defines, entry.key_ptr.*, entry.value_ptr.*, false, false, log, allocator); + } + + for (drop) |drop_item| { + if (drop_item.len > 0) { + try fromMergeableInputEntry(&user_defines, drop_item, "", true, true, log, allocator); + } + } return user_defines; } @@ -170,6 +184,7 @@ const inf_val = js_ast.E.Number{ .value = std.math.inf(f64) }; pub const Define = struct { identifiers: bun.StringHashMap(IdentifierDefine), dots: bun.StringHashMap([]DotDefine), + drop_debugger: bool, allocator: std.mem.Allocator, pub const Data = DefineData; @@ -236,11 +251,12 @@ pub const Define = struct { } } - pub fn init(allocator: std.mem.Allocator, _user_defines: ?UserDefines, string_defines: ?UserDefinesArray) bun.OOM!*@This() { - var define = try allocator.create(Define); + pub fn init(allocator: std.mem.Allocator, _user_defines: ?UserDefines, string_defines: ?UserDefinesArray, drop_debugger: bool) bun.OOM!*@This() { + const define = try allocator.create(Define); define.allocator = allocator; define.identifiers = bun.StringHashMap(IdentifierDefine).init(allocator); define.dots = bun.StringHashMap([]DotDefine).init(allocator); + define.drop_debugger = drop_debugger; try define.dots.ensureTotalCapacity(124); const value_define = DefineData{ diff --git a/src/js_parser.zig b/src/js_parser.zig index 4c9ce810bd..2e01434404 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -2509,12 +2509,8 @@ const ExprIn = struct { // Currently this is only used when unwrapping a call to `require()` // with `__toESM()`. is_immediately_assigned_to_decl: bool = false, -}; -const ExprOut = struct { - // True if the child node is an optional chain node (EDot, EIndex, or ECall - // with an IsOptionalChain value of true) - child_contains_optional_chain: bool = false, + property_access_for_method_call_maybe_should_replace_with_undefined: bool = false, }; const Tup = std.meta.Tuple; @@ -4871,6 +4867,8 @@ fn NewParser_( /// We must be careful to avoid revisiting nodes that have scopes. is_revisit_for_substitution: bool = false, + method_call_must_be_replaced_with_undefined: bool = false, + // Inside a TypeScript namespace, an "export declare" statement can be used // to cause a namespace to be emitted even though it has no other observable // effect. This flag is used to implement this feature. @@ -16318,6 +16316,11 @@ fn NewParser_( if (def.call_can_be_unwrapped_if_unused and !p.options.ignore_dce_annotations) { e_.call_can_be_unwrapped_if_unused = true; } + + // If the user passed --drop=console, drop all property accesses to console. + if (def.method_call_must_be_replaced_with_undefined and in.property_access_for_method_call_maybe_should_replace_with_undefined and in.assign_target == .none) { + p.method_call_must_be_replaced_with_undefined = true; + } } // Substitute uncalled "require" for the require target @@ -16988,6 +16991,10 @@ fn NewParser_( if (!define.data.valueless) { return p.valueForDefine(expr.loc, in.assign_target, is_delete_target, &define.data); } + + if (define.data.method_call_must_be_replaced_with_undefined and in.property_access_for_method_call_maybe_should_replace_with_undefined) { + p.method_call_must_be_replaced_with_undefined = true; + } } // Copy the side effect flags over in case this expression is unused @@ -17019,7 +17026,9 @@ fn NewParser_( } } - e_.target = p.visitExpr(e_.target); + e_.target = p.visitExprInOut(e_.target, .{ + .property_access_for_method_call_maybe_should_replace_with_undefined = in.property_access_for_method_call_maybe_should_replace_with_undefined, + }); // 'require.resolve' -> .e_require_resolve_call_target if (e_.target.data == .e_require_call_target and @@ -17291,6 +17300,7 @@ fn NewParser_( const target_was_identifier_before_visit = e_.target.data == .e_identifier; e_.target = p.visitExprInOut(e_.target, .{ .has_chain_parent = e_.optional_chain == .continuation, + .property_access_for_method_call_maybe_should_replace_with_undefined = true, }); // Copy the call side effect flag over if this is a known target @@ -17346,6 +17356,7 @@ fn NewParser_( defer p.options.ignore_dce_annotations = old_ce; const old_should_fold_typescript_constant_expressions = p.should_fold_typescript_constant_expressions; defer p.should_fold_typescript_constant_expressions = old_should_fold_typescript_constant_expressions; + const old_is_control_flow_dead = p.is_control_flow_dead; // We want to forcefully fold constants inside of // certain calls even when minification is disabled, so @@ -17362,9 +17373,29 @@ fn NewParser_( p.should_fold_typescript_constant_expressions = true; } + var method_call_should_be_replaced_with_undefined = p.method_call_must_be_replaced_with_undefined; + + if (method_call_should_be_replaced_with_undefined) { + p.method_call_must_be_replaced_with_undefined = false; + switch (e_.target.data) { + // If we're removing this call, don't count any arguments as symbol uses + .e_index, .e_dot => { + p.is_control_flow_dead = true; + }, + else => { + method_call_should_be_replaced_with_undefined = false; + }, + } + } + for (e_.args.slice()) |*arg| { arg.* = p.visitExpr(arg.*); } + + if (method_call_should_be_replaced_with_undefined) { + p.is_control_flow_dead = old_is_control_flow_dead; + return .{ .data = .{ .e_undefined = .{} }, .loc = expr.loc }; + } } if (e_.target.data == .e_require_call_target) { @@ -18948,7 +18979,13 @@ fn NewParser_( switch (stmt.data) { // These don't contain anything to traverse - .s_debugger, .s_empty, .s_comment => { + .s_debugger => { + p.current_scope.is_after_const_local_prefix = was_after_after_const_local_prefix; + if (p.define.drop_debugger) { + return; + } + }, + .s_empty, .s_comment => { p.current_scope.is_after_const_local_prefix = was_after_after_const_local_prefix; }, .s_type_script => { diff --git a/src/js_printer.zig b/src/js_printer.zig index 08199425c2..699a1ed684 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -5804,7 +5804,7 @@ const FileWriterInternal = struct { ctx: *FileWriterInternal, ) anyerror!void { defer buffer.reset(); - const result_ = buffer.toOwnedSliceLeaky(); + const result_ = buffer.slice(); var result = result_; while (result.len > 0) { @@ -5954,10 +5954,10 @@ pub const BufferWriter = struct { } if (ctx.append_null_byte) { - ctx.sentinel = ctx.buffer.toOwnedSentinelLeaky(); - ctx.written = ctx.buffer.toOwnedSliceLeaky(); + ctx.sentinel = ctx.buffer.sliceWithSentinel(); + ctx.written = ctx.buffer.slice(); } else { - ctx.written = ctx.buffer.toOwnedSliceLeaky(); + ctx.written = ctx.buffer.slice(); } } diff --git a/src/options.zig b/src/options.zig index ace0f854e0..b779186472 100644 --- a/src/options.zig +++ b/src/options.zig @@ -1162,6 +1162,7 @@ pub fn definesFromTransformOptions( env_loader: ?*DotEnv.Loader, framework_env: ?*const Env, NODE_ENV: ?string, + drop: []const []const u8, ) !*defines.Define { const input_user_define = maybe_input_define orelse std.mem.zeroes(Api.StringMap); @@ -1252,12 +1253,17 @@ pub fn definesFromTransformOptions( } } - const resolved_defines = try defines.DefineData.fromInput(user_defines, log, allocator); + const resolved_defines = try defines.DefineData.fromInput(user_defines, drop, log, allocator); + + const drop_debugger = for (drop) |item| { + if (strings.eqlComptime(item, "debugger")) break true; + } else false; return try defines.Define.init( allocator, resolved_defines, environment_defines, + drop_debugger, ); } @@ -1420,6 +1426,7 @@ pub const BundleOptions = struct { footer: string = "", banner: string = "", define: *defines.Define, + drop: []const []const u8 = &.{}, loaders: Loader.HashTable, resolve_dir: string = "/", jsx: JSX.Pragma = JSX.Pragma{}, @@ -1579,6 +1586,7 @@ pub const BundleOptions = struct { break :node_env "\"development\""; }, + this.drop, ); this.defines_loaded = true; } @@ -1680,6 +1688,7 @@ pub const BundleOptions = struct { .env = Env.init(allocator), .transform_options = transform, .experimental_css = false, + .drop = transform.drop, }; Analytics.Features.define += @as(usize, @intFromBool(transform.define != null)); diff --git a/src/renamer.zig b/src/renamer.zig index 64e9e93e56..c41e4ca66c 100644 --- a/src/renamer.zig +++ b/src/renamer.zig @@ -751,9 +751,9 @@ pub const NumberRenamer = struct { mutable_name.appendSlice(prefix) catch unreachable; mutable_name.appendInt(tries) catch unreachable; - switch (NameUse.find(this, mutable_name.toOwnedSliceLeaky())) { + switch (NameUse.find(this, mutable_name.slice())) { .unused => { - name = mutable_name.toOwnedSliceLeaky(); + name = mutable_name.slice(); if (use == .same_scope) { const existing = this.name_counts.getOrPut(allocator, prefix) catch unreachable; @@ -775,7 +775,7 @@ pub const NumberRenamer = struct { tries += 1; - switch (NameUse.find(this, mutable_name.toOwnedSliceLeaky())) { + switch (NameUse.find(this, mutable_name.slice())) { .unused => { if (cur_use == .same_scope) { const existing = this.name_counts.getOrPut(allocator, prefix) catch unreachable; @@ -790,7 +790,7 @@ pub const NumberRenamer = struct { existing.value_ptr.* = tries; } - name = mutable_name.toOwnedSliceLeaky(); + name = mutable_name.slice(); break; }, else => {}, @@ -847,7 +847,7 @@ pub const ExportRenamer = struct { var writer = this.string_buffer.writer(); writer.print("{s}{d}", .{ input, tries }) catch unreachable; tries += 1; - const attempt = this.string_buffer.toOwnedSliceLeaky(); + const attempt = this.string_buffer.slice(); entry = this.used.getOrPut(attempt) catch unreachable; if (!entry.found_existing) { const to_use = this.string_buffer.allocator.dupe(u8, attempt) catch unreachable; diff --git a/src/sourcemap/CodeCoverage.zig b/src/sourcemap/CodeCoverage.zig index eb3b4e0343..52d3624143 100644 --- a/src/sourcemap/CodeCoverage.zig +++ b/src/sourcemap/CodeCoverage.zig @@ -695,7 +695,7 @@ pub const ByteRangeMapping = struct { return .zero; }; - var str = bun.String.createUTF8(mutable_str.toOwnedSliceLeaky()); + var str = bun.String.createUTF8(mutable_str.slice()); defer str.deref(); return str.toJS(globalThis); } diff --git a/src/string_mutable.zig b/src/string_mutable.zig index d787c9a3ab..042184d501 100644 --- a/src/string_mutable.zig +++ b/src/string_mutable.zig @@ -37,8 +37,8 @@ pub const MutableString = struct { } } - pub fn owns(this: *const MutableString, slice: []const u8) bool { - return bun.isSliceInBuffer(slice, this.list.items.ptr[0..this.list.capacity]); + pub fn owns(this: *const MutableString, items: []const u8) bool { + return bun.isSliceInBuffer(items, this.list.items.ptr[0..this.list.capacity]); } pub fn growIfNeeded(self: *MutableString, amount: usize) OOM!void { @@ -119,8 +119,8 @@ pub const MutableString = struct { str[0..start_i]); needs_gap = false; - var slice = str[start_i..]; - iterator = strings.CodepointIterator.init(slice); + var items = str[start_i..]; + iterator = strings.CodepointIterator.init(items); cursor = strings.CodepointIterator.Cursor{}; while (iterator.next(&cursor)) { @@ -130,7 +130,7 @@ pub const MutableString = struct { needs_gap = false; has_needed_gap = true; } - try mutable.append(slice[cursor.i .. cursor.i + @as(u32, cursor.width)]); + try mutable.append(items[cursor.i .. cursor.i + @as(u32, cursor.width)]); } else if (!needs_gap) { needs_gap = true; // skip the code point, replace it with a single _ @@ -172,17 +172,16 @@ pub const MutableString = struct { try self.list.ensureUnusedCapacity(self.allocator, amount); } - pub inline fn appendSlice(self: *MutableString, slice: []const u8) !void { - try self.list.appendSlice(self.allocator, slice); + pub inline fn appendSlice(self: *MutableString, items: []const u8) !void { + try self.list.appendSlice(self.allocator, items); } - pub inline fn appendSliceExact(self: *MutableString, slice: []const u8) !void { - if (slice.len == 0) return; - - try self.list.ensureTotalCapacityPrecise(self.allocator, self.list.items.len + slice.len); + pub inline fn appendSliceExact(self: *MutableString, items: []const u8) !void { + if (items.len == 0) return; + try self.list.ensureTotalCapacityPrecise(self.allocator, self.list.items.len + items.len); var end = self.list.items.ptr + self.list.items.len; - self.list.items.len += slice.len; - @memcpy(end[0..slice.len], slice); + self.list.items.len += items.len; + @memcpy(end[0..items.len], items); } pub inline fn reset( @@ -237,7 +236,7 @@ pub const MutableString = struct { return self.list.toOwnedSlice(self.allocator) catch bun.outOfMemory(); // TODO } - pub fn toOwnedSliceLeaky(self: *MutableString) []u8 { + pub fn slice(self: *MutableString) []u8 { return self.list.items; } @@ -248,7 +247,8 @@ pub const MutableString = struct { return out; } - pub fn toOwnedSentinelLeaky(self: *MutableString) [:0]u8 { + /// Appends `0` if needed + pub fn sliceWithSentinel(self: *MutableString) [:0]u8 { if (self.list.items.len > 0 and self.list.items[self.list.items.len - 1] != 0) { self.list.append( self.allocator, @@ -264,10 +264,6 @@ pub const MutableString = struct { return self.list.toOwnedSlice(self.allocator) catch bun.outOfMemory(); // TODO } - // pub fn deleteAt(self: *MutableString, i: usize) { - // self.list.swapRemove(i); - // } - pub fn containsChar(self: *const MutableString, char: u8) bool { return self.indexOfChar(char) != null; } @@ -399,46 +395,46 @@ pub const MutableString = struct { } pub fn writeHTMLAttributeValue(this: *BufferedWriter, bytes: []const u8) anyerror!void { - var slice = bytes; - while (slice.len > 0) { + var items = bytes; + while (items.len > 0) { // TODO: SIMD - if (strings.indexOfAny(slice, "\"<>")) |j| { - _ = try this.writeAll(slice[0..j]); - _ = switch (slice[j]) { + if (strings.indexOfAny(items, "\"<>")) |j| { + _ = try this.writeAll(items[0..j]); + _ = switch (items[j]) { '"' => try this.writeAll("""), '<' => try this.writeAll("<"), '>' => try this.writeAll(">"), else => unreachable, }; - slice = slice[j + 1 ..]; + items = items[j + 1 ..]; continue; } - _ = try this.writeAll(slice); + _ = try this.writeAll(items); break; } } pub fn writeHTMLAttributeValue16(this: *BufferedWriter, bytes: []const u16) anyerror!void { - var slice = bytes; - while (slice.len > 0) { - if (strings.indexOfAny16(slice, "\"<>")) |j| { + var items = bytes; + while (items.len > 0) { + if (strings.indexOfAny16(items, "\"<>")) |j| { // this won't handle strings larger than 4 GB // that's fine though, 4 GB of SSR'd HTML is quite a lot... - _ = try this.writeAll16(slice[0..j]); - _ = switch (slice[j]) { + _ = try this.writeAll16(items[0..j]); + _ = switch (items[j]) { '"' => try this.writeAll("""), '<' => try this.writeAll("<"), '>' => try this.writeAll(">"), else => unreachable, }; - slice = slice[j + 1 ..]; + items = items[j + 1 ..]; continue; } - _ = try this.writeAll16(slice); + _ = try this.writeAll16(items); break; } } diff --git a/test/bundler/bundler_drop.test.ts b/test/bundler/bundler_drop.test.ts new file mode 100644 index 0000000000..a50bda9f58 --- /dev/null +++ b/test/bundler/bundler_drop.test.ts @@ -0,0 +1,109 @@ +import { describe } from 'bun:test'; +import { itBundled } from "./expectBundled"; + +describe("bundler", () => { + itBundled("drop/FunctionCall", { + files: { + "/a.js": `console.log("hello");`, + }, + run: { stdout: "" }, + drop: ["console"], + backend: "api", + }); + itBundled("drop/DebuggerStmt", { + files: { + "/a.js": `if(true){debugger;debugger;};debugger;function y(){ debugger; }y()`, + }, + drop: ["debugger"], + backend: "api", + onAfterBundle(api) { + api.expectFile("out.js").not.toInclude("debugger"); + }, + }); + itBundled("drop/NoDisableDebugger", { + files: { + "/a.js": `if(true){debugger;debugger;};debugger;function y(){ debugger; }y();`, + }, + backend: "api", + onAfterBundle(api) { + api.expectFile("out.js").toIncludeRepeated("debugger", 4); + }, + }); + itBundled("drop/RemovesSideEffects", { + files: { + "/a.js": `console.log(alert());`, + }, + run: { stdout: "" }, + drop: ["console"], + backend: "api", + }); + itBundled("drop/ReassignKeepsOutput", { + files: { + "/a.js": `var call = console.log; call("hello");`, + }, + run: { stdout: "hello" }, + drop: ["console"], + backend: "api", + }); + itBundled("drop/AssignKeepsOutput", { + files: { + "/a.js": `var call = console.log("a"); globalThis.console.log(call);`, + }, + run: { stdout: "undefined" }, + drop: ["console"], + backend: "api", + }); + itBundled("drop/UnaryExpression", { + files: { + "/a.js": `Bun.inspect(); console.log("hello");`, + }, + run: { stdout: "" }, + drop: ["console"], + backend: "api", + }); + itBundled("drop/0Args", { + files: { + "/a.js": `console.log();`, + }, + run: { stdout: "" }, + drop: ["console"], + }); + itBundled("drop/BecomesUndefined", { + files: { + "/a.js": `console.log(Bun.inspect.table());`, + }, + run: { stdout: "undefined" }, + drop: ["Bun.inspect.table"], + }); + itBundled("drop/BecomesUndefinedNested1", { + files: { + "/a.js": `console.log(Bun.inspect.table());`, + }, + run: { stdout: "undefined" }, + drop: ["Bun.inspect"], + }); + itBundled("drop/BecomesUndefinedNested2", { + files: { + "/a.js": `console.log(Bun.inspect.table());`, + }, + run: { stdout: "undefined" }, + drop: ["Bun"], + }); + itBundled("drop/AssignTarget", { + files: { + "/a.js": `console.log( + ( + Bun.inspect.table = (() => 123) + )());`, + }, + run: { stdout: "123" }, + drop: ["Bun"], + }); + itBundled("drop/DeleteAssignTarget", { + files: { + "/a.js": `console.log((delete Bun.inspect()));`, + }, + run: { stdout: "true" }, + drop: ["Bun"], + }); +}); diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index e1dca71531..e4ede264ef 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -148,6 +148,7 @@ export interface BundlerTestInput { banner?: string; footer?: string; define?: Record; + drop?: string[]; /** Use for resolve custom conditions */ conditions?: string[]; @@ -416,6 +417,7 @@ function expectBundled( env, external, packages, + drop = [], files, footer, format, @@ -653,6 +655,7 @@ function expectBundled( minifyIdentifiers && `--minify-identifiers`, minifySyntax && `--minify-syntax`, minifyWhitespace && `--minify-whitespace`, + drop?.length && drop.map(x => ["--drop=" + x]), experimentalCss && "--experimental-css", globalName && `--global-name=${globalName}`, jsx.runtime && ["--jsx-runtime", jsx.runtime], @@ -790,6 +793,7 @@ function expectBundled( delete bundlerEnv[key]; } } + const { stdout, stderr, success, exitCode } = Bun.spawnSync({ cmd, cwd: root, @@ -988,6 +992,7 @@ function expectBundled( publicPath, emitDCEAnnotations, ignoreDCEAnnotations, + drop, } as BuildConfig; if (conditions?.length) {