From c2cf52895321a40f680316a59bb252a15e9c1b12 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Thu, 1 Aug 2024 15:00:38 -0700 Subject: [PATCH] bundler: Add `--ignore-dce-annotations`, and other DCE annotation related stuff (#12808) Co-authored-by: paperdave Co-authored-by: Jarred Sumner --- docs/bundler/vs-esbuild.md | 3 +- src/api/schema.zig | 3 + src/bun_js.zig | 2 + src/bundler.zig | 5 + src/bundler/bundle_v2.zig | 9 + src/cli.zig | 13 +- src/cli/build_command.zig | 6 + src/js_parser.zig | 40 ++-- src/js_printer.zig | 16 +- src/options.zig | 3 + test/bundler/esbuild/dce.test.ts | 302 +++++++++++++++----------- test/bundler/esbuild/tsconfig.test.ts | 4 +- test/bundler/expectBundled.ts | 6 + test/cli/install/bun-run.test.ts | 40 +++- 14 files changed, 286 insertions(+), 166 deletions(-) diff --git a/docs/bundler/vs-esbuild.md b/docs/bundler/vs-esbuild.md index 83dccc4ecb0d07..da2905bf6e52f1 100644 --- a/docs/bundler/vs-esbuild.md +++ b/docs/bundler/vs-esbuild.md @@ -208,8 +208,7 @@ In Bun's CLI, simple boolean flags like `--minify` do not accept an argument. Ot --- - `--ignore-annotations` -- n/a -- Not supported +- `--ignore-dce-annotations` --- diff --git a/src/api/schema.zig b/src/api/schema.zig index 8bed348e9b1946..713aefc7138c45 100644 --- a/src/api/schema.zig +++ b/src/api/schema.zig @@ -1687,6 +1687,9 @@ pub const Api = struct { /// packages packages: ?PackagesMode = null, + /// ignore_dce_annotations + ignore_dce_annotations: bool, + pub fn decode(reader: anytype) anyerror!TransformOptions { var this = std.mem.zeroes(TransformOptions); diff --git a/src/bun_js.zig b/src/bun_js.zig index 8947e3721880fc..66cc3399364d2d 100644 --- a/src/bun_js.zig +++ b/src/bun_js.zig @@ -88,6 +88,7 @@ pub const Run = struct { b.options.minify_identifiers = ctx.bundler_options.minify_identifiers; b.options.minify_whitespace = ctx.bundler_options.minify_whitespace; + b.options.ignore_dce_annotations = ctx.bundler_options.ignore_dce_annotations; b.resolver.opts.minify_identifiers = ctx.bundler_options.minify_identifiers; b.resolver.opts.minify_whitespace = ctx.bundler_options.minify_whitespace; @@ -232,6 +233,7 @@ pub const Run = struct { b.options.minify_identifiers = ctx.bundler_options.minify_identifiers; b.options.minify_whitespace = ctx.bundler_options.minify_whitespace; + b.options.ignore_dce_annotations = ctx.bundler_options.ignore_dce_annotations; b.resolver.opts.minify_identifiers = ctx.bundler_options.minify_identifiers; b.resolver.opts.minify_whitespace = ctx.bundler_options.minify_whitespace; diff --git a/src/bundler.zig b/src/bundler.zig index ac6a81203c9823..cd88138e94d0dc 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -1144,6 +1144,7 @@ pub const Bundler = struct { .minify_identifiers = bundler.options.minify_identifiers, .transform_only = bundler.options.transform_only, .runtime_transpiler_cache = runtime_transpiler_cache, + .print_dce_annotations = bundler.options.emit_dce_annotations, }, enable_source_map, ), @@ -1167,6 +1168,7 @@ pub const Bundler = struct { .transform_only = bundler.options.transform_only, .import_meta_ref = ast.import_meta_ref, .runtime_transpiler_cache = runtime_transpiler_cache, + .print_dce_annotations = bundler.options.emit_dce_annotations, }, enable_source_map, ), @@ -1199,6 +1201,7 @@ pub const Bundler = struct { .inline_require_and_import_errors = false, .import_meta_ref = ast.import_meta_ref, .runtime_transpiler_cache = runtime_transpiler_cache, + .print_dce_annotations = bundler.options.emit_dce_annotations, }, enable_source_map, ), @@ -1405,6 +1408,8 @@ pub const Bundler = struct { opts.features.runtime_transpiler_cache = this_parse.runtime_transpiler_cache; opts.transform_only = bundler.options.transform_only; + opts.ignore_dce_annotations = bundler.options.ignore_dce_annotations; + // @bun annotation opts.features.dont_bundle_twice = this_parse.dont_bundle_twice; diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 74776675ce2718..7706ec1321a484 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -760,6 +760,9 @@ pub const BundleV2 = struct { generator.linker.options.minify_syntax = bundler.options.minify_syntax; generator.linker.options.minify_identifiers = bundler.options.minify_identifiers; generator.linker.options.minify_whitespace = bundler.options.minify_whitespace; + generator.linker.options.emit_dce_annotations = bundler.options.emit_dce_annotations; + generator.linker.options.ignore_dce_annotations = bundler.options.ignore_dce_annotations; + generator.linker.options.source_maps = bundler.options.source_map; generator.linker.options.tree_shaking = bundler.options.tree_shaking; generator.linker.options.public_path = bundler.options.public_path; @@ -1627,6 +1630,7 @@ pub const BundleV2 = struct { .extension_order = &.{}, .env_files = &.{}, .conditions = config.conditions.map.keys(), + .ignore_dce_annotations = bundler.options.ignore_dce_annotations, }, completion.env, ); @@ -2874,6 +2878,7 @@ pub const ParseTask = struct { opts.features.minify_syntax = bundler.options.minify_syntax; opts.features.minify_identifiers = bundler.options.minify_identifiers; opts.features.emit_decorator_metadata = bundler.options.emit_decorator_metadata; + opts.ignore_dce_annotations = bundler.options.ignore_dce_annotations and !source.index.isRuntime(); opts.tree_shaking = if (source.index.isRuntime()) true else bundler.options.tree_shaking; opts.module_type = task.module_type; @@ -3851,6 +3856,7 @@ pub const LinkerContext = struct { pub const LinkerOptions = struct { output_format: options.OutputFormat = .esm, ignore_dce_annotations: bool = false, + emit_dce_annotations: bool = true, tree_shaking: bool = true, minify_whitespace: bool = false, minify_syntax: bool = false, @@ -6804,6 +6810,7 @@ pub const LinkerContext = struct { .minify_whitespace = c.options.minify_whitespace, .minify_identifiers = c.options.minify_identifiers, .minify_syntax = c.options.minify_syntax, + .print_dce_annotations = c.options.emit_dce_annotations, // .const_values = c.graph.const_values, }; @@ -7714,6 +7721,7 @@ pub const LinkerContext = struct { .require_or_import_meta_for_source_callback = js_printer.RequireOrImportMeta.Callback.init(LinkerContext, requireOrImportMetaForSource, c), .minify_whitespace = c.options.minify_whitespace, + .print_dce_annotations = c.options.emit_dce_annotations, .minify_syntax = c.options.minify_syntax, // .const_values = c.graph.const_values, }; @@ -9015,6 +9023,7 @@ pub const LinkerContext = struct { .minify_whitespace = c.options.minify_whitespace, .minify_syntax = c.options.minify_syntax, .module_type = c.options.output_format, + .print_dce_annotations = c.options.emit_dce_annotations, .has_run_symbol_renamer = true, .allocator = allocator, diff --git a/src/cli.zig b/src/cli.zig index d7135e36122380..5de5c1e2d192b7 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -185,6 +185,7 @@ pub const Arguments = struct { clap.parseParam("--jsx-fragment Changes the function called when compiling JSX fragments") catch unreachable, clap.parseParam("--jsx-import-source Declares the module specifier to be used for importing the jsx and jsxs factory functions. Default: \"react\"") catch unreachable, clap.parseParam("--jsx-runtime \"automatic\" (default) or \"classic\"") catch unreachable, + clap.parseParam("--ignore-dce-annotations Ignore tree-shaking annotations such as @__PURE__") catch unreachable, }; const runtime_params_ = [_]ParamType{ clap.parseParam("--watch Automatically restart the process on file change") catch unreachable, @@ -238,7 +239,7 @@ pub const Arguments = struct { clap.parseParam("--no-clear-screen Disable clearing the terminal screen on reload when --watch is enabled") catch unreachable, clap.parseParam("--target The intended execution environment for the bundle. \"browser\", \"bun\" or \"node\"") catch unreachable, clap.parseParam("--outdir Default to \"dist\" if multiple files") catch unreachable, - clap.parseParam("--outfile Write to a file") catch unreachable, + clap.parseParam("--outfile Write to a file") catch unreachable, clap.parseParam("--sourcemap ? Build with sourcemaps - 'inline', 'external', or 'none'") catch unreachable, clap.parseParam("--format Specifies the module format to build to. Only \"esm\" is supported.") catch unreachable, clap.parseParam("--root Root directory used for multiple entry points") catch unreachable, @@ -251,10 +252,11 @@ pub const Arguments = struct { clap.parseParam("--asset-naming Customize asset filenames. Defaults to \"[name]-[hash].[ext]\"") catch unreachable, clap.parseParam("--server-components Enable React Server Components (experimental)") catch unreachable, clap.parseParam("--no-bundle Transpile file only, do not bundle") catch unreachable, + clap.parseParam("--emit-dce-annotations Re-emit DCE annotations in bundles. Enabled by default unless --minify-whitespace is passed.") catch unreachable, clap.parseParam("--minify Enable all minification flags") catch unreachable, clap.parseParam("--minify-syntax Minify syntax and inline data") catch unreachable, clap.parseParam("--minify-whitespace Minify whitespace") catch unreachable, - clap.parseParam("--minify-identifiers Minify identifiers") catch unreachable, + clap.parseParam("--minify-identifiers Minify identifiers") catch unreachable, clap.parseParam("--dump-environment-variables") catch unreachable, clap.parseParam("--conditions ... Pass custom conditions to resolve") catch unreachable, }; @@ -697,6 +699,8 @@ pub const Arguments = struct { const output_dir: ?string = null; const output_file: ?string = null; + ctx.bundler_options.ignore_dce_annotations = args.flag("--ignore-dce-annotations"); + if (cmd == .BuildCommand) { ctx.bundler_options.transform_only = args.flag("--no-bundle"); @@ -709,6 +713,9 @@ pub const Arguments = struct { ctx.bundler_options.minify_whitespace = minify_flag or args.flag("--minify-whitespace"); ctx.bundler_options.minify_identifiers = minify_flag or args.flag("--minify-identifiers"); + ctx.bundler_options.emit_dce_annotations = args.flag("--emit-dce-annotations") or + !ctx.bundler_options.minify_whitespace; + if (args.options("--external").len > 0) { var externals = try allocator.alloc([]u8, args.options("--external").len); for (args.options("--external"), 0..) |external, i| { @@ -1275,6 +1282,8 @@ pub const Command = struct { minify_syntax: bool = false, minify_whitespace: bool = false, minify_identifiers: bool = false, + ignore_dce_annotations: bool = false, + emit_dce_annotations: bool = true, }; pub fn create(allocator: std.mem.Allocator, log: *logger.Log, comptime command: Command.Tag) anyerror!Context { diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig index 631278298854f7..5fee90e015fc88 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -114,6 +114,12 @@ pub const BuildCommand = struct { this_bundler.options.minify_identifiers = ctx.bundler_options.minify_identifiers; this_bundler.resolver.opts.minify_identifiers = ctx.bundler_options.minify_identifiers; + this_bundler.options.emit_dce_annotations = ctx.bundler_options.emit_dce_annotations; + this_bundler.resolver.opts.emit_dce_annotations = ctx.bundler_options.emit_dce_annotations; + + this_bundler.options.ignore_dce_annotations = ctx.bundler_options.ignore_dce_annotations; + this_bundler.resolver.opts.ignore_dce_annotations = ctx.bundler_options.ignore_dce_annotations; + if (ctx.bundler_options.compile) { if (ctx.bundler_options.code_splitting) { Output.prettyErrorln("error: cannot use --compile with --splitting", .{}); diff --git a/src/js_parser.zig b/src/js_parser.zig index e7322655bde21a..370d6bb5792c0d 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -1935,13 +1935,14 @@ pub const SideEffects = enum(u1) { } }, - .e_call => |call| { - + inline .e_call, .e_new => |call| { // A call that has been marked "__PURE__" can be removed if all arguments // can be removed. The annotation causes us to ignore the target. if (call.can_be_unwrapped_if_unused) { if (call.args.len > 0) { return Expr.joinAllWithCommaCallback(call.args.slice(), @TypeOf(p), p, comptime simplifyUnusedExpr, p.allocator); + } else { + return Expr.empty; } } }, @@ -2071,23 +2072,6 @@ pub const SideEffects = enum(u1) { ); }, - .e_new => |call| { - // A constructor call that has been marked "__PURE__" can be removed if all arguments - // can be removed. The annotation causes us to ignore the target. - if (call.can_be_unwrapped_if_unused) { - if (call.args.len > 0) { - return Expr.joinAllWithCommaCallback( - call.args.slice(), - @TypeOf(p), - p, - comptime simplifyUnusedExpr, - p.allocator, - ); - } - - return null; - } - }, else => {}, } @@ -3122,6 +3106,10 @@ pub const Parser = struct { hasher.update("NO_TS"); } + if (this.ignore_dce_annotations) { + hasher.update("no_dce"); + } + this.features.hashForRuntimeTranspiler(hasher); } @@ -13201,12 +13189,6 @@ fn NewParser_( .e_new => |ex| { ex.can_be_unwrapped_if_unused = true; }, - - // this is specifically added only to support our implementation - // of '__require' for --target=node, for /* @__PURE__ */ import.meta.url - .e_dot => |ex| { - ex.can_be_removed_if_unused = true; - }, else => {}, } } @@ -20860,7 +20842,13 @@ fn NewParser_( E.Call{ .target = target, .args = ExprNodeList.init(args_list), - .can_be_unwrapped_if_unused = all_values_are_pure, + // TODO: make these fully tree-shakable. this annotation + // as-is is incorrect. This would be done by changing all + // enum wrappers into `var Enum = ...` instead of two + // separate statements. This way, the @__PURE__ annotation + // is attached to the variable binding. + // + // .can_be_unwrapped_if_unused = all_values_are_pure, }, stmt_loc, ); diff --git a/src/js_printer.zig b/src/js_printer.zig index 47f431d5c860d8..6a636edffc067f 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -65,8 +65,8 @@ const ascii_only_always_on_unless_minifying = true; fn formatUnsignedIntegerBetween(comptime len: u16, buf: *[len]u8, val: u64) void { comptime var i: u16 = len; var remainder = val; - // Write out the number from the end to the front + // Write out the number from the end to the front inline while (i > 0) { comptime i -= 1; buf[comptime i] = @as(u8, @intCast((remainder % 10))) + '0'; @@ -536,6 +536,8 @@ pub const Options = struct { minify_whitespace: bool = false, minify_identifiers: bool = false, minify_syntax: bool = false, + print_dce_annotations: bool = true, + transform_only: bool = false, inline_require_and_import_errors: bool = true, has_run_symbol_renamer: bool = false, @@ -544,7 +546,7 @@ pub const Options = struct { module_type: options.OutputFormat = .preserve, - /// Used for cross-module inlining of import items when bundling + // /// Used for cross-module inlining of import items when bundling // const_values: Ast.ConstValuesMap = .{}, ts_enums: Ast.TsEnumsMap = .{}, @@ -2146,8 +2148,10 @@ fn NewPrinter( return; } - // noop for now - pub inline fn printPure(_: *Printer) void {} + pub inline fn printPure(p: *Printer) void { + if (Environment.allow_assert) assert(p.options.print_dce_annotations); + p.printWhitespacer(ws("/* @__PURE__ */ ")); + } pub fn printQuotedUTF8(p: *Printer, str: string, allow_backtick: bool) void { const quote = if (comptime !is_json) @@ -2353,7 +2357,7 @@ fn NewPrinter( } }, .e_new => |e| { - const has_pure_comment = e.can_be_unwrapped_if_unused; + const has_pure_comment = e.can_be_unwrapped_if_unused and p.options.print_dce_annotations; const wrap = level.gte(.call) or (has_pure_comment and level.gte(.postfix)); if (wrap) { @@ -2403,7 +2407,7 @@ fn NewPrinter( wrap = true; } - const has_pure_comment = e.can_be_unwrapped_if_unused; + const has_pure_comment = e.can_be_unwrapped_if_unused and p.options.print_dce_annotations; if (has_pure_comment and level.gte(.postfix)) { wrap = true; } diff --git a/src/options.zig b/src/options.zig index b2b60949cb701a..9a7abd576350f0 100644 --- a/src/options.zig +++ b/src/options.zig @@ -1515,6 +1515,9 @@ pub const BundleOptions = struct { minify_identifiers: bool = false, dead_code_elimination: bool = true, + ignore_dce_annotations: bool = false, + emit_dce_annotations: bool = false, + code_coverage: bool = false, debugger: bool = false, diff --git a/test/bundler/esbuild/dce.test.ts b/test/bundler/esbuild/dce.test.ts index 69e80e8ce3885a..a4a5b84250b7aa 100644 --- a/test/bundler/esbuild/dce.test.ts +++ b/test/bundler/esbuild/dce.test.ts @@ -1,5 +1,6 @@ import { itBundled, dedent } from "../expectBundled"; import { describe, expect } from "bun:test"; +import { isWindows } from 'harness'; // Tests ported from: // https://github.com/evanw/esbuild/blob/main/internal/bundler_tests/bundler_dce_test.go @@ -107,7 +108,6 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsTrueKeepES6", { - todo: true, files: { "/Users/user/project/src/entry.js": /* js */ ` import "demo-pkg" @@ -129,7 +129,6 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsTrueKeepCommonJS", { - todo: true, files: { "/Users/user/project/src/entry.js": /* js */ ` import "demo-pkg" @@ -151,7 +150,6 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsFalseKeepBareImportAndRequireES6", { - todo: true, files: { "/Users/user/project/src/entry.js": /* js */ ` import "demo-pkg" @@ -174,7 +172,6 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsFalseKeepBareImportAndRequireCommonJS", { - todo: true, files: { "/Users/user/project/src/entry.js": /* js */ ` import "demo-pkg" @@ -197,7 +194,6 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsFalseRemoveBareImportES6", { - todo: true, files: { "/Users/user/project/src/entry.js": /* js */ ` import "demo-pkg" @@ -219,7 +215,6 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsFalseRemoveBareImportCommonJS", { - todo: true, files: { "/Users/user/project/src/entry.js": /* js */ ` import "demo-pkg" @@ -241,7 +236,6 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsFalseRemoveNamedImportES6", { - todo: true, files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" @@ -263,7 +257,6 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsFalseRemoveNamedImportCommonJS", { - todo: true, files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" @@ -285,7 +278,6 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsFalseRemoveStarImportES6", { - todo: true, files: { "/Users/user/project/src/entry.js": /* js */ ` import * as ns from "demo-pkg" @@ -307,7 +299,6 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsFalseRemoveStarImportCommonJS", { - todo: true, files: { "/Users/user/project/src/entry.js": /* js */ ` import * as ns from "demo-pkg" @@ -351,7 +342,7 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsArrayKeep", { - todo: true, + todo: isWindows, files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" @@ -429,7 +420,6 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsArrayKeepMainImplicitModule", { - todo: true, files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" @@ -457,7 +447,6 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsArrayKeepMainImplicitMain", { - todo: true, files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" @@ -490,7 +479,7 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsArrayKeepModuleUseModule", { - todo: true, + todo: isWindows, files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" @@ -518,7 +507,7 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsArrayKeepModuleUseMain", { - todo: true, + todo: isWindows, files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" @@ -546,7 +535,7 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsArrayKeepModuleImplicitModule", { - todo: true, + todo: isWindows, files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" @@ -778,7 +767,6 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsFalseIntermediateFilesDiamond", { - todo: true, files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "a" @@ -807,7 +795,6 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsFalseOneFork", { - todo: true, files: { "/Users/user/project/src/entry.js": `import("a").then(x => console.log(x.foo))`, "/Users/user/project/node_modules/a/index.js": `export {foo} from "b"`, @@ -828,7 +815,6 @@ describe("bundler", () => { }, }); itBundled("dce/PackageJsonSideEffectsFalseAllFork", { - todo: true, files: { "/Users/user/project/src/entry.js": `import("a").then(x => console.log(x.foo))`, "/Users/user/project/node_modules/a/index.js": `export {foo} from "b"`, @@ -851,7 +837,6 @@ describe("bundler", () => { }, }); itBundled("dce/JSONLoaderRemoveUnused", { - todo: true, files: { "/entry.js": /* js */ ` import unused from "./example.json" @@ -865,7 +850,6 @@ describe("bundler", () => { }, }); itBundled("dce/TextLoaderRemoveUnused", { - todo: true, files: { "/entry.js": /* js */ ` import unused from "./example.txt" @@ -934,7 +918,6 @@ describe("bundler", () => { }, }); itBundled("dce/RemoveUnusedImportMeta", { - todo: true, files: { "/entry.js": /* js */ ` function foo() { @@ -948,109 +931,119 @@ describe("bundler", () => { stdout: "foo is unused", }, }); - itBundled("dce/RemoveUnusedPureCommentCalls", { - todo: true, - // in this test, the bundler must drop all `_yes` variables entirely, and then - // preserve the pure comments in the same way esbuild does - files: { - "/entry.js": /* js */ ` - function bar() {} - let bare = foo(bar); - - let at_yes = /* @__PURE__ */ foo(bar); - let at_no = /* @__PURE__ */ foo(bar()); - let new_at_yes = /* @__PURE__ */ new foo(bar); - let new_at_no = /* @__PURE__ */ new foo(bar()); - - let nospace_at_yes = /*@__PURE__*/ foo(bar); - let nospace_at_no = /*@__PURE__*/ foo(bar()); - let nospace_new_at_yes = /*@__PURE__*/ new foo(bar); - let nospace_new_at_no = /*@__PURE__*/ new foo(bar()); - - let num_yes = /* #__PURE__ */ foo(bar); - let num_no = /* #__PURE__ */ foo(bar()); - let new_num_yes = /* #__PURE__ */ new foo(bar); - let new_num_no = /* #__PURE__ */ new foo(bar()); - - let nospace_num_yes = /*#__PURE__*/ foo(bar); - let nospace_num_no = /*#__PURE__*/ foo(bar()); - let nospace_new_num_yes = /*#__PURE__*/ new foo(bar); - let nospace_new_num_no = /*#__PURE__*/ new foo(bar()); - - let dot_yes = /* @__PURE__ */ foo(sideEffect()).dot(bar); - let dot_no = /* @__PURE__ */ foo(sideEffect()).dot(bar()); - let new_dot_yes = /* @__PURE__ */ new foo(sideEffect()).dot(bar); - let new_dot_no = /* @__PURE__ */ new foo(sideEffect()).dot(bar()); - - let nested_yes = [1, /* @__PURE__ */ foo(bar), 2]; - let nested_no = [1, /* @__PURE__ */ foo(bar()), 2]; - let new_nested_yes = [1, /* @__PURE__ */ new foo(bar), 2]; - let new_nested_no = [1, /* @__PURE__ */ new foo(bar()), 2]; - - let single_at_yes = // @__PURE__ - foo(bar); - let single_at_no = // @__PURE__ - foo(bar()); - let new_single_at_yes = // @__PURE__ - new foo(bar); - let new_single_at_no = // @__PURE__ - new foo(bar()); - - let single_num_yes = // #__PURE__ - foo(bar); - let single_num_no = // #__PURE__ - foo(bar()); - let new_single_num_yes = // #__PURE__ - new foo(bar); - let new_single_num_no = // #__PURE__ - new foo(bar()); - - let bad_no = /* __PURE__ */ foo(bar); - let new_bad_no = /* __PURE__ */ new foo(bar); - - let parens_no = (/* @__PURE__ */ foo)(bar); - let new_parens_no = new (/* @__PURE__ */ foo)(bar); - - let exp_no = /* @__PURE__ */ foo() ** foo(); - let new_exp_no = /* @__PURE__ */ new foo() ** foo(); - `, - }, - onAfterBundle(api) { - const code = api.readFile("/out.js"); - expect(code).not.toContain("_yes"); // should not contain any *_yes variables - expect(code).toContain("var bare = foo(bar)"); // should contain `var bare = foo(bar)` - const keep = [ - ["at_no", true], - ["new_at_no", true], - ["nospace_at_no", true], - ["nospace_new_at_no", true], - ["num_no", true], - ["new_num_no", true], - ["nospace_num_no", true], - ["nospace_new_num_no", true], - ["dot_no", true], - ["new_dot_no", true], - ["nested_no", true], - ["new_nested_no", true], - ["single_at_no", true], - ["new_single_at_no", true], - ["single_num_no", true], - ["new_single_num_no", true], - ["bad_no", false], - ["new_bad_no", false], - ["parens_no", false], - ["new_parens_no", false], - ["exp_no", true], - ["new_exp_no", true], - ]; - for (const [name, pureComment] of keep) { - const regex = new RegExp(`${name}\\s*=[^\/\n]*(\\/\\*.*?\\*\\/)?`, "g"); - const match = regex.exec(code); - expect(match).toBeTruthy(); // should contain ${name} - expect(pureComment ? !!match[1] : !match[1]).toBeTruthy(); // should contain a pure comment for ${name} - } - }, - }); + for (const { minify, emitDCEAnnotations, name } of [ + { minify: false, emitDCEAnnotations: false, name: "dce/RemoveUnusedPureCommentCalls" }, + { minify: true, emitDCEAnnotations: false, name: "dce/RemoveUnusedPureCommentCallsMinify" }, + { minify: true, emitDCEAnnotations: true, name: "dce/RemoveUnusedPureCommentCallsMinifyExplitOn" }, + ]) { + itBundled(name, { + // in this test, the bundler must drop all `_yes` variables entirely, and then + // preserve the pure comments in the same way esbuild does + files: { + "/entry.js": /* js */ ` + function bar() {} + let bare = foo(bar); + + let at_yes = /* @__PURE__ */ foo(bar); + let at_no = /* @__PURE__ */ foo(bar()); + let new_at_yes = /* @__PURE__ */ new foo(bar); + let new_at_no = /* @__PURE__ */ new foo(bar()); + + let nospace_at_yes = /*@__PURE__*/ foo(bar); + let nospace_at_no = /*@__PURE__*/ foo(bar()); + let nospace_new_at_yes = /*@__PURE__*/ new foo(bar); + let nospace_new_at_no = /*@__PURE__*/ new foo(bar()); + + let num_yes = /* #__PURE__ */ foo(bar); + let num_no = /* #__PURE__ */ foo(bar()); + let new_num_yes = /* #__PURE__ */ new foo(bar); + let new_num_no = /* #__PURE__ */ new foo(bar()); + + let nospace_num_yes = /*#__PURE__*/ foo(bar); + let nospace_num_no = /*#__PURE__*/ foo(bar()); + let nospace_new_num_yes = /*#__PURE__*/ new foo(bar); + let nospace_new_num_no = /*#__PURE__*/ new foo(bar()); + + let dot_yes = /* @__PURE__ */ foo(sideEffect()).dot(bar); + let dot_no = /* @__PURE__ */ foo(sideEffect()).dot(bar()); + let new_dot_yes = /* @__PURE__ */ new foo(sideEffect()).dot(bar); + let new_dot_no = /* @__PURE__ */ new foo(sideEffect()).dot(bar()); + + let nested_yes = [1, /* @__PURE__ */ foo(bar), 2]; + let nested_no = [1, /* @__PURE__ */ foo(bar()), 2]; + let new_nested_yes = [1, /* @__PURE__ */ new foo(bar), 2]; + let new_nested_no = [1, /* @__PURE__ */ new foo(bar()), 2]; + + let single_at_yes = // @__PURE__ + foo(bar); + let single_at_no = // @__PURE__ + foo(bar()); + let new_single_at_yes = // @__PURE__ + new foo(bar); + let new_single_at_no = // @__PURE__ + new foo(bar()); + + let single_num_yes = // #__PURE__ + foo(bar); + let single_num_no = // #__PURE__ + foo(bar()); + let new_single_num_yes = // #__PURE__ + new foo(bar); + let new_single_num_no = // #__PURE__ + new foo(bar()); + + let bad_no = /* __PURE__ */ foo(bar); + let new_bad_no = /* __PURE__ */ new foo(bar); + + let parens_no = (/* @__PURE__ */ foo)(bar); + let new_parens_no = new (/* @__PURE__ */ foo)(bar); + + let exp_no = /* @__PURE__ */ foo() ** foo(); + let new_exp_no = /* @__PURE__ */ new foo() ** foo(); + `, + }, + minifyWhitespace: minify, + emitDCEAnnotations: emitDCEAnnotations, + onAfterBundle(api) { + const code = api.readFile("/out.js"); + expect(code).not.toContain("_yes"); // should not contain any *_yes variables + expect(code).toContain(minify ? "var bare=foo(bar)" : "var bare = foo(bar)"); + const keep = [ + ["at_no", true], + ["new_at_no", true], + ["nospace_at_no", true], + ["nospace_new_at_no", true], + ["num_no", true], + ["new_num_no", true], + ["nospace_num_no", true], + ["nospace_new_num_no", true], + ["dot_no", true], + ["new_dot_no", true], + ["nested_no", true], + ["new_nested_no", true], + ["single_at_no", true], + ["new_single_at_no", true], + ["single_num_no", true], + ["new_single_num_no", true], + ["parens_no", false], + ["new_parens_no", false], + ["exp_no", true], + ["new_exp_no", true], + ]; + for (const [name, pureComment] of keep) { + const regex = new RegExp(`${name}\\s*=[^\/\n;]*(\\/\\*[^\/\n;]*?PURE[^\/\n;]*?\\*\\/)?`, "g"); + const match = regex.exec(code)!; + expect(match).toBeTruthy(); // should contain ${name} + + if ((emitDCEAnnotations || !minify) && pureComment) { + expect(match[1], "should contain pure comment for " + name).toBeTruthy(); + } else { + expect(match[1], "should not contain pure comment for " + name).toBeFalsy(); + } + } + }, + }); + } itBundled("dce/TreeShakingReactElements", { files: { "/entry.jsx": /* jsx */ ` @@ -2608,7 +2601,6 @@ describe("bundler", () => { }, }); itBundled("dce/CrossModuleConstantFolding", { - todo: true, files: { "/enum-constants.ts": /* ts */ ` export enum remove { @@ -2718,7 +2710,6 @@ describe("bundler", () => { dce: true, }); itBundled("dce/MultipleDeclarationTreeShaking", { - todo: true, files: { "/var2.js": /* js */ ` var x = 1 @@ -2757,7 +2748,6 @@ describe("bundler", () => { ], }); itBundled("dce/MultipleDeclarationTreeShakingMinifySyntax", { - todo: true, files: { "/var2.js": /* js */ ` var x = 1 @@ -2964,6 +2954,62 @@ describe("bundler", () => { stdout: "foo\nbar", }, }); + itBundled("dce/CallWithNoArg", { + files: { + "/entry.js": /* js */ ` + /* @__PURE__ */ noSideEffects(); + `, + }, + run: { + stdout: "", + }, + }); + itBundled("dce/ConstructWithNoArg", { + files: { + "/entry.js": /* js */ ` + /* @__PURE__ */ new NoSideEffects(); + `, + }, + run: { + stdout: "", + }, + }); + itBundled("dce/IgnoreAnnotations", { + files: { + "/entry.js": /* js */ ` + function noSideEffects() { console.log("PASS"); } + /* @__PURE__ */ noSideEffects(1); + `, + }, + ignoreDCEAnnotations: true, + run: { + stdout: "PASS", + }, + }); + itBundled("dce/IgnoreAnnotationsDoesNotApplyToRuntime", { + files: { + "/entry.js": /* js */ ` + import("./other.js"); + `, + "/other.js": /* js */ ` + export function foo() { } + `, + }, + ignoreDCEAnnotations: true, + onAfterBundle(api) { + // These symbols technically have side effects, and we use dce annotations + // to let them tree-shake User-specified --ignore-annotations should not + // apply to our code. + api.expectFile("/out.js").not.toContain("__dispose"); + api.expectFile("/out.js").not.toContain("__asyncDispose"); + api.expectFile("/out.js").not.toContain("__require"); + + // This assertion catches if the bundler changes in that the runtime is no + // longer included. If this fails, just adjust the code snippet so some + // part of runtime.js is used + api.expectFile("/out.js").toContain("__defProp"); + }, + }); // itBundled("dce/TreeShakingJSWithAssociatedCSS", { // // TODO: css assertions. this should contain both button and menu // files: { diff --git a/test/bundler/esbuild/tsconfig.test.ts b/test/bundler/esbuild/tsconfig.test.ts index bdeb28d462086d..d96d822f96bad8 100644 --- a/test/bundler/esbuild/tsconfig.test.ts +++ b/test/bundler/esbuild/tsconfig.test.ts @@ -393,7 +393,9 @@ describe("bundler", () => { onAfterBundle(api) { api .expectFile("/Users/user/project/out.js") - .toContain(`console.log(R.c(R.F, null, R.c(\"div\", null), R.c(\"div\", null)));\n`); + .toContain( + `console.log(/* @__PURE__ */ R.c(R.F, null, /* @__PURE__ */ R.c(\"div\", null), /* @__PURE__ */ R.c(\"div\", null)));\n`, + ); }, }); itBundled("tsconfig/ReactJSXNotReact", { diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index ce8c8131de0385..7e93a791fdc3ea 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -165,6 +165,7 @@ export interface BundlerTestInput { format?: "esm" | "cjs" | "iife"; globalName?: string; ignoreDCEAnnotations?: boolean; + emitDCEAnnotations?: boolean; inject?: string[]; jsx?: { runtime?: "automatic" | "classic"; @@ -440,6 +441,8 @@ function expectBundled( unsupportedCSSFeatures, unsupportedJSFeatures, useDefineForClassFields, + ignoreDCEAnnotations, + emitDCEAnnotations, // @ts-expect-error _referenceFn, expectExactFilesize, @@ -645,6 +648,8 @@ function expectBundled( splitting && `--splitting`, serverComponents && "--server-components", outbase && `--root=${outbase}`, + ignoreDCEAnnotations && `--ignore-dce-annotations`, + emitDCEAnnotations && `--emit-dce-annotations`, // inject && inject.map(x => ["--inject", path.join(root, x)]), // jsx.preserve && "--jsx=preserve", // legalComments && `--legal-comments=${legalComments}`, @@ -687,6 +692,7 @@ function expectBundled( sourceMap && `--sourcemap=${sourceMap}`, banner && `--banner:js=${banner}`, legalComments && `--legal-comments=${legalComments}`, + ignoreDCEAnnotations && `--ignore-annotations`, splitting && `--splitting`, treeShaking && `--tree-shaking`, outbase && `--outbase=${outbase}`, diff --git a/test/cli/install/bun-run.test.ts b/test/cli/install/bun-run.test.ts index 203f8f9676072f..21f48c4c9f84c8 100644 --- a/test/cli/install/bun-run.test.ts +++ b/test/cli/install/bun-run.test.ts @@ -1,6 +1,6 @@ import { file, spawn, spawnSync } from "bun"; import { afterEach, beforeEach, expect, it, describe } from "bun:test"; -import { bunEnv, bunExe, bunEnv as env, isWindows, tmpdirSync } from "harness"; +import { bunEnv, bunExe, bunEnv as env, isWindows, tempDirWithFiles, tmpdirSync } from "harness"; import { rm, writeFile, exists, mkdir } from "fs/promises"; import { join } from "path"; import { readdirSorted } from "./dummy.registry"; @@ -437,3 +437,41 @@ it("should show the correct working directory when run with --cwd", async () => expect(await res.exited).toBe(0); expect(await Bun.readableStreamToText(res.stdout)).toMatch(/subdir/); }); + +it("DCE annotations are respected", () => { + const dir = tempDirWithFiles("test", { + "index.ts": ` + /* @__PURE__ */ console.log("Hello, world!"); + `, + }); + + const { stdout, stderr, exitCode } = spawnSync({ + cmd: [bunExe(), "run", "index.ts"], + cwd: dir, + env: bunEnv, + }); + + expect(exitCode).toBe(0); + + expect(stderr.toString()).toBe(""); + expect(stdout.toString()).toBe(""); +}); + +it("--ignore-dce-annotations ignores DCE annotations", () => { + const dir = tempDirWithFiles("test", { + "index.ts": ` + /* @__PURE__ */ console.log("Hello, world!"); + `, + }); + + const { stdout, stderr, exitCode } = spawnSync({ + cmd: [bunExe(), "--ignore-dce-annotations", "run", "index.ts"], + cwd: dir, + env: bunEnv, + }); + + expect(exitCode).toBe(0); + + expect(stderr.toString()).toBe(""); + expect(stdout.toString()).toBe("Hello, world!\n"); +});