diff options
author | 2022-04-16 04:36:00 -0700 | |
---|---|---|
committer | 2022-04-16 04:36:00 -0700 | |
commit | 42414d56678d10e9a658da55f83b41e8c751a1c1 (patch) | |
tree | 4905d6be2cc9f1f7179f380eeb15288db9cdb33a | |
parent | 8bb283e61674ef06b0eac1bd0ade04e6560e0131 (diff) | |
download | bun-42414d56678d10e9a658da55f83b41e8c751a1c1.tar.gz bun-42414d56678d10e9a658da55f83b41e8c751a1c1.tar.zst bun-42414d56678d10e9a658da55f83b41e8c751a1c1.zip |
[JS Parser] API for removing & replacing exports
-rw-r--r-- | integration/bunjs-only-snippets/transpiler.test.js | 73 | ||||
-rw-r--r-- | src/js_parser/js_parser.zig | 507 |
2 files changed, 495 insertions, 85 deletions
diff --git a/integration/bunjs-only-snippets/transpiler.test.js b/integration/bunjs-only-snippets/transpiler.test.js index 9d570ea50..c0646f578 100644 --- a/integration/bunjs-only-snippets/transpiler.test.js +++ b/integration/bunjs-only-snippets/transpiler.test.js @@ -1,6 +1,79 @@ import { expect, it, describe } from "bun:test"; describe("Bun.Transpiler", () => { + describe("replaceExports", () => { + const transpiler = new Bun.Transpiler({ + exports: { + replace: { + // Next.js does this + getStaticProps: ["__N_SSG", true], + localVarToReplace: 2, + }, + // Remix could possibly do this when building for browsers + // to automatically remove imports only referenced within the loader + // For Remix, it probably is less impactful due to .client and .server conventions in place + eliminate: ["loader", "localVarToRemove"], + }, + treeShaking: true, + trimUnusedImports: true, + }); + it("deletes dead exports and any imports only referenced in dead regions", () => { + const output = transpiler.transformSync(` + import deadFS from 'fs'; + import liveFS from 'fs'; + + export function loader() { + deadFS.readFileSync("/etc/passwd"); + liveFS.readFileSync("/etc/passwd"); + } + + export function action() { + require("foo"); + liveFS.readFileSync("/etc/passwd"); + } + + export default function() { + require("bar"); + } + `); + expect(output.includes("loader")).toBe(false); + expect(output.includes("react")).toBe(false); + expect(output.includes("action")).toBe(true); + expect(output.includes("deadFS")).toBe(false); + expect(output.includes("liveFS")).toBe(true); + }); + + it("supports replacing exports", () => { + const output = transpiler.transformSync(` + import deadFS from 'fs'; + import anotherDeadFS from 'fs'; + import liveFS from 'fs'; + + export var localVarToRemove = deadFS.readFileSync("/etc/passwd"); + export var localVarToReplace = 1; + + var getStaticProps = function () { + deadFS.readFileSync("/etc/passwd") + }; + + export {getStaticProps} + + export default function() { + liveFS.readFileSync("/etc/passwd"); + require("bar"); + } + `); + expect(output.includes("loader")).toBe(false); + expect(output.includes("react")).toBe(false); + expect(output.includes("deadFS")).toBe(false); + expect(output.includes("anotherDeadFS")).toBe(false); + expect(output.includes("liveFS")).toBe(true); + expect(output.includes("__N_SSG")).toBe(true); + expect(output.includes("localVarToReplace")).toBe(true); + expect(output.includes("localVarToRemove")).toBe(false); + }); + }); + const transpiler = new Bun.Transpiler({ loader: "tsx", define: { diff --git a/src/js_parser/js_parser.zig b/src/js_parser/js_parser.zig index d6cb1523c..f96b13206 100644 --- a/src/js_parser/js_parser.zig +++ b/src/js_parser/js_parser.zig @@ -492,9 +492,8 @@ pub const ImportScanner = struct { // user is expecting the output to be as small as possible. So we // should omit unused imports. // - // const keep_unused_imports = !p.options.trim_unused_imports; var did_remove_star_loc = false; - const keep_unused_imports = !is_typescript_enabled; + const keep_unused_imports = !p.options.features.trim_unused_imports; // TypeScript always trims unused imports. This is important for // correctness since some imports might be fake (only in the type @@ -537,6 +536,7 @@ pub const ImportScanner = struct { if (p.import_items_for_namespace.get(st.namespace_ref)) |entry| { if (entry.count() > 0) { has_any = true; + break; } } @@ -595,7 +595,9 @@ pub const ImportScanner = struct { // e.g. `import 'fancy-stylesheet-thing/style.css';` // This is a breaking change though. We can make it an option with some guardrail // so maybe if it errors, it shows a suggestion "retry without trimming unused imports" - if (is_typescript_enabled and found_imports and is_unused_in_typescript and !p.options.preserve_unused_imports_ts) { + if ((is_typescript_enabled and found_imports and is_unused_in_typescript and !p.options.preserve_unused_imports_ts) or + (!is_typescript_enabled and p.options.features.trim_unused_imports and found_imports and st.star_name_loc == null and st.items.len == 0 and st.default_name == null)) + { // internal imports are presumed to be always used // require statements cannot be stripped if (!record.is_internal and !record.was_originally_require) { @@ -1929,6 +1931,8 @@ const RefCtx = @import("../ast/base.zig").RefCtx; const SymbolUseMap = std.HashMapUnmanaged(Ref, js_ast.Symbol.Use, RefCtx, 80); const StringBoolMap = std.StringHashMapUnmanaged(bool); const RefMap = std.HashMapUnmanaged(Ref, void, RefCtx, 80); +const RefArrayMap = std.ArrayHashMapUnmanaged(Ref, void, @import("../ast/base.zig").RefHashCtx, false); + const RefRefMap = std.HashMapUnmanaged(Ref, Ref, RefCtx, 80); const ImportRecord = importRecord.ImportRecord; const Flags = js_ast.Flags; @@ -2162,7 +2166,6 @@ pub const Parser = struct { transform_require_to_import: bool = true, moduleType: ModuleType = ModuleType.esm, - trim_unused_imports: bool = true, pub fn init(jsx: options.JSX.Pragma, loader: options.Loader) Options { var opts = Options{ @@ -3266,6 +3269,7 @@ fn NewParser_( injected_define_symbols: List(Ref) = .{}, symbol_uses: js_ast.Part.SymbolUseMap = .{}, declared_symbols: List(js_ast.DeclaredSymbol) = .{}, + declared_symbols_for_reuse: List(js_ast.DeclaredSymbol) = .{}, runtime_imports: RuntimeImports = RuntimeImports{}, parse_pass_symbol_uses: ParsePassSymbolUsageType = undefined, @@ -3469,6 +3473,8 @@ fn NewParser_( scope_order_to_visit: []ScopeOrder = &([_]ScopeOrder{}), + import_refs_to_always_trim_if_unused: RefArrayMap = .{}, + pub fn transposeImport(p: *P, arg: Expr, state: anytype) Expr { // The argument must be a string if (@as(Expr.Tag, arg.data) == .e_string) { @@ -3667,7 +3673,8 @@ fn NewParser_( .s_local => |local| { if (local.is_export) break :can_remove_part false; for (local.decls) |decl| { - if (isBindingUsed(p, decl.binding, default_export_ref)) break :can_remove_part false; + if (isBindingUsed(p, decl.binding, default_export_ref)) + break :can_remove_part false; } }, .s_if => |if_statement| { @@ -3734,17 +3741,7 @@ fn NewParser_( }; if (is_dead) { - var symbol_use_refs = part.symbol_uses.keys(); - var symbol_use_values = part.symbol_uses.values(); - - for (symbol_use_refs) |ref, i| { - p.symbols.items[ref.innerIndex()].use_count_estimate -|= symbol_use_values[i].count_estimate; - } - - for (part.declared_symbols) |declared| { - p.symbols.items[declared.ref.innerIndex()].use_count_estimate = 0; - // } - } + p.clearSymbolUsagesFromDeadPart(part); continue; } @@ -3752,6 +3749,7 @@ fn NewParser_( parts_[parts_end] = part; parts_end += 1; } + parts_.len = parts_end; if (last_end == parts_.len) { break; @@ -3768,6 +3766,21 @@ fn NewParser_( pub const Hoisted = Binding.ToExpr(P, P.wrapIdentifierHoisting); }; + fn clearSymbolUsagesFromDeadPart(p: *P, part: js_ast.Part) void { + var symbol_use_refs = part.symbol_uses.keys(); + var symbol_use_values = part.symbol_uses.values(); + var symbols = p.symbols.items; + + for (symbol_use_refs) |ref, i| { + symbols[ref.innerIndex()].use_count_estimate -|= symbol_use_values[i].count_estimate; + } + + for (part.declared_symbols) |declared| { + symbols[declared.ref.innerIndex()].use_count_estimate = 0; + // } + } + } + pub fn s(_: *P, t: anytype, loc: logger.Loc) Stmt { const Type = @TypeOf(t); comptime { @@ -11600,16 +11613,14 @@ fn NewParser_( } fn appendPart(p: *P, parts: *ListManaged(js_ast.Part), stmts: []Stmt) !void { - p.symbol_uses = js_ast.Part.SymbolUseMap{}; + // Reuse the memory if possible + // This is reusable if the last part turned out to be dead + p.symbol_uses.clearRetainingCapacity(); + p.declared_symbols.clearRetainingCapacity(); + p.scopes_for_current_part.clearRetainingCapacity(); + p.import_records_for_current_part.clearRetainingCapacity(); + const allocator = p.allocator; - p.declared_symbols.deinit( - allocator, - ); - p.declared_symbols = @TypeOf(p.declared_symbols){}; - p.import_records_for_current_part.deinit(allocator); - p.import_records_for_current_part = @TypeOf(p.import_records_for_current_part){}; - p.scopes_for_current_part.deinit(allocator); - p.scopes_for_current_part = @TypeOf(p.scopes_for_current_part){}; var opts = PrependTempRefsOpts{}; var partStmts = ListManaged(Stmt).fromOwnedSlice(allocator, stmts); try p.visitStmtsAndPrependTempRefs(&partStmts, &opts); @@ -11617,6 +11628,9 @@ fn NewParser_( // Insert any relocated variable statements now if (p.relocated_top_level_vars.items.len > 0) { var already_declared = RefMap{}; + var already_declared_allocator_stack = std.heap.stackFallback(1024, allocator); + var already_declared_allocator = already_declared_allocator_stack.get(); + defer if (already_declared_allocator_stack.fixed_buffer_allocator.end_index >= 1023) already_declared.deinit(already_declared_allocator); for (p.relocated_top_level_vars.items) |*local| { // Follow links because "var" declarations may be merged due to hoisting @@ -11628,10 +11642,8 @@ fn NewParser_( local.ref = link; } const ref = local.ref orelse continue; - var declaration_entry = try already_declared.getOrPut(allocator, ref); + var declaration_entry = try already_declared.getOrPut(already_declared_allocator, ref); if (!declaration_entry.found_existing) { - try already_declared.put(allocator, ref, .{}); - const decls = try allocator.alloc(G.Decl, 1); decls[0] = Decl{ .binding = p.b(B.Identifier{ .ref = ref }, local.loc), @@ -11639,8 +11651,7 @@ fn NewParser_( try partStmts.append(p.s(S.Local{ .decls = decls }, local.loc)); } } - p.relocated_top_level_vars.deinit(allocator); - p.relocated_top_level_vars = @TypeOf(p.relocated_top_level_vars){}; + p.relocated_top_level_vars.clearRetainingCapacity(); // Follow links because "var" declarations may be merged due to hoisting @@ -11664,6 +11675,10 @@ fn NewParser_( .scopes = p.scopes_for_current_part.toOwnedSlice(p.allocator), .can_be_removed_if_unused = p.stmtsCanBeRemovedIfUnused(_stmts), }); + p.symbol_uses = .{}; + } else if (p.declared_symbols.items.len > 0 or p.symbol_uses.count() > 0) { + // if the part is dead, invalidate all the usage counts + p.clearSymbolUsagesFromDeadPart(.{ .stmts = undefined, .declared_symbols = p.declared_symbols.items, .symbol_uses = p.symbol_uses }); } } @@ -13934,33 +13949,68 @@ fn NewParser_( } }, .s_export_clause => |data| { - // "export {foo}" var end: usize = 0; - for (data.items) |*item| { - const name = p.loadNameFromRef(item.name.ref.?); - const symbol = try p.findSymbol(item.alias_loc, name); - const ref = symbol.ref; - - if (p.symbols.items[ref.innerIndex()].kind == .unbound) { - // Silently strip exports of non-local symbols in TypeScript, since - // those likely correspond to type-only exports. But report exports of - // non-local symbols as errors in JavaScript. - if (!is_typescript_enabled) { - const r = js_lexer.rangeOfIdentifier(p.source, item.name.loc); - try p.log.addRangeErrorFmt(p.source, r, p.allocator, "\"{s}\" is not declared in this file", .{name}); + var any_replaced = false; + if (p.options.features.replace_exports.count() > 0) { + for (data.items) |*item| { + const name = p.loadNameFromRef(item.name.ref.?); + + const symbol = try p.findSymbol(item.alias_loc, name); + const ref = symbol.ref; + + if (p.options.features.replace_exports.getPtr(name)) |entry| { + if (entry.* != .replace) p.ignoreUsage(symbol.ref); + _ = p.injectReplacementExport(stmts, symbol.ref, stmt.loc, entry); + any_replaced = true; continue; } - continue; + + if (p.symbols.items[ref.innerIndex()].kind == .unbound) { + // Silently strip exports of non-local symbols in TypeScript, since + // those likely correspond to type-only exports. But report exports of + // non-local symbols as errors in JavaScript. + if (!is_typescript_enabled) { + const r = js_lexer.rangeOfIdentifier(p.source, item.name.loc); + try p.log.addRangeErrorFmt(p.source, r, p.allocator, "\"{s}\" is not declared in this file", .{name}); + } + continue; + } + + item.name.ref = ref; + data.items[end] = item.*; + end += 1; + } + } else { + for (data.items) |*item| { + const name = p.loadNameFromRef(item.name.ref.?); + const symbol = try p.findSymbol(item.alias_loc, name); + const ref = symbol.ref; + + if (p.symbols.items[ref.innerIndex()].kind == .unbound) { + // Silently strip exports of non-local symbols in TypeScript, since + // those likely correspond to type-only exports. But report exports of + // non-local symbols as errors in JavaScript. + if (!is_typescript_enabled) { + const r = js_lexer.rangeOfIdentifier(p.source, item.name.loc); + try p.log.addRangeErrorFmt(p.source, r, p.allocator, "\"{s}\" is not declared in this file", .{name}); + continue; + } + continue; + } + + item.name.ref = ref; + data.items[end] = item.*; + end += 1; } + } + + const remove_for_tree_shaking = any_replaced and end == 0 and data.items.len > 0 and p.options.tree_shaking; + data.items.len = end; - item.name.ref = ref; - data.items[end] = item.*; - end += 1; + if (remove_for_tree_shaking) { + return; } - // esbuild: "Note: do not remove empty export statements since TypeScript uses them as module markers" - // jarred: does that mean we can remove them here, since we're not bundling for production? - data.items = data.items[0..end]; }, .s_export_from => |data| { // When HMR is enabled, we need to transform this into @@ -13980,13 +14030,45 @@ fn NewParser_( try p.current_scope.generated.append(p.allocator, data.namespace_ref); try p.recordDeclaredSymbol(data.namespace_ref); - // This is a re-export and the symbols created here are used to reference - for (data.items) |*item| { - const _name = p.loadNameFromRef(item.name.ref.?); - const ref = try p.newSymbol(.other, _name); - try p.current_scope.generated.append(p.allocator, data.namespace_ref); - try p.recordDeclaredSymbol(data.namespace_ref); - item.name.ref = ref; + if (p.options.features.replace_exports.count() > 0) { + var j: usize = 0; + // This is a re-export and the symbols created here are used to reference + for (data.items) |item| { + const old_ref = item.name.ref.?; + + if (p.options.features.replace_exports.count() > 0) { + if (p.options.features.replace_exports.getPtr(item.alias)) |entry| { + _ = p.injectReplacementExport(stmts, old_ref, logger.Loc.Empty, entry); + + continue; + } + } + + const _name = p.loadNameFromRef(old_ref); + + const ref = try p.newSymbol(.other, _name); + try p.current_scope.generated.append(p.allocator, data.namespace_ref); + try p.recordDeclaredSymbol(data.namespace_ref); + data.items[j] = item; + data.items[j].name.ref = ref; + j += 1; + } + + data.items.len = j; + + if (j == 0 and data.items.len > 0) { + return; + } + } else { + + // This is a re-export and the symbols created here are used to reference + for (data.items) |*item| { + const _name = p.loadNameFromRef(item.name.ref.?); + const ref = try p.newSymbol(.other, _name); + try p.current_scope.generated.append(p.allocator, data.namespace_ref); + try p.recordDeclaredSymbol(data.namespace_ref); + item.name.ref = ref; + } } }, .s_export_star => |data| { @@ -13999,6 +14081,12 @@ fn NewParser_( // "export * as ns from 'path'" if (data.alias) |alias| { + if (p.options.features.replace_exports.count() > 0) { + if (p.options.features.replace_exports.getPtr(alias.original_name)) |entry| { + _ = p.injectReplacementExport(stmts, p.declareSymbol(.other, logger.Loc.Empty, alias.original_name) catch unreachable, logger.Loc.Empty, entry); + return; + } + } // "import * as ns from 'path'" // "export {ns}" @@ -14020,12 +14108,31 @@ fn NewParser_( try p.recordDeclaredSymbol(ref); } + var mark_for_replace: bool = false; + + const orig_dead = p.is_control_flow_dead; + if (p.options.features.replace_exports.count() > 0) { + if (p.options.features.replace_exports.getPtr("default")) |entry| { + p.is_control_flow_dead = entry.* != .replace; + mark_for_replace = true; + } + } + + defer { + p.is_control_flow_dead = orig_dead; + } + switch (data.value) { .expr => |expr| { const was_anonymous_named_expr = p.isAnonymousNamedExpr(expr); + data.value.expr = p.visitExpr(expr); - // // Optionally preserve the name + if (p.is_control_flow_dead) { + return; + } + + // Optionally preserve the name data.value.expr = p.maybeKeepExprSymbolName(data.value.expr, js_ast.ClauseItem.default_alias, was_anonymous_named_expr); @@ -14048,6 +14155,16 @@ fn NewParser_( } } + if (mark_for_replace) { + var entry = p.options.features.replace_exports.getPtr("default").?; + if (entry.* == .replace) { + data.value.expr = entry.replace; + } else { + _ = p.injectReplacementExport(stmts, Ref.None, logger.Loc.Empty, entry); + return; + } + } + // When bundling, replace ExportDefault with __exportDefault(exportsRef, expr); if (p.options.enable_bundling) { var export_default_args = p.allocator.alloc(Expr, 2) catch unreachable; @@ -14072,7 +14189,28 @@ fn NewParser_( func.func = p.visitFunc(func.func, func.func.open_parens_loc); - if (p.options.enable_bundling) { + if (p.is_control_flow_dead) { + return; + } + + if (mark_for_replace) { + var entry = p.options.features.replace_exports.getPtr("default").?; + if (entry.* == .replace) { + data.value = .{ .expr = entry.replace }; + } else { + _ = p.injectReplacementExport(stmts, Ref.None, logger.Loc.Empty, entry); + return; + } + + // When bundling, replace ExportDefault with __exportDefault(exportsRef, expr); + if (p.options.enable_bundling) { + var export_default_args = p.allocator.alloc(Expr, 2) catch unreachable; + export_default_args[0] = p.@"module.exports"(data.value.expr.loc); + export_default_args[1] = data.value.expr; + stmts.append(p.s(S.SExpr{ .value = p.callRuntime(data.value.expr.loc, "__exportDefault", export_default_args) }, data.value.expr.loc)) catch unreachable; + return; + } + } else if (p.options.enable_bundling) { var export_default_args = p.allocator.alloc(Expr, 2) catch unreachable; export_default_args[0] = p.@"module.exports"(s2.loc); @@ -14101,7 +14239,27 @@ fn NewParser_( // TODO: https://github.com/Jarred-Sumner/bun/issues/51 _ = p.visitClass(s2.loc, &class.class); - if (p.options.enable_bundling) { + if (p.is_control_flow_dead) + return; + + if (mark_for_replace) { + var entry = p.options.features.replace_exports.getPtr("default").?; + if (entry.* == .replace) { + data.value = .{ .expr = entry.replace }; + } else { + _ = p.injectReplacementExport(stmts, Ref.None, logger.Loc.Empty, entry); + return; + } + + // When bundling, replace ExportDefault with __exportDefault(exportsRef, expr); + if (p.options.enable_bundling) { + var export_default_args = p.allocator.alloc(Expr, 2) catch unreachable; + export_default_args[0] = p.@"module.exports"(data.value.expr.loc); + export_default_args[1] = data.value.expr; + stmts.append(p.s(S.SExpr{ .value = p.callRuntime(data.value.expr.loc, "__exportDefault", export_default_args) }, data.value.expr.loc)) catch unreachable; + return; + } + } else if (p.options.enable_bundling) { var export_default_args = p.allocator.alloc(Expr, 2) catch unreachable; export_default_args[0] = p.@"module.exports"(s2.loc); @@ -14200,29 +14358,18 @@ fn NewParser_( p.popScope(); }, .s_local => |data| { - var i: usize = 0; - while (i < data.decls.len) : (i += 1) { - p.visitBinding(data.decls[i].binding, null); - - if (data.decls[i].value != null) { - var val = data.decls[i].value.?; - const was_anonymous_named_expr = p.isAnonymousNamedExpr(val); - - const prev_macro_call_count = p.macro_call_count; - - data.decls[i].value = p.visitExpr(val); + const decls_len = if (!(data.is_export and p.options.features.replace_exports.entries.len > 0)) + p.visitDecls(data.decls, false) + else + p.visitDecls(data.decls, true); - p.visitDecl( - &data.decls[i], - was_anonymous_named_expr, - if (comptime allow_macros) - prev_macro_call_count != p.macro_call_count - else - false, - ); - } + const is_now_dead = data.decls.len > 0 and decls_len == 0; + if (is_now_dead) { + return; } + data.decls.len = decls_len; + // Handle being exported inside a namespace if (data.is_export and p.enclosing_namespace_arg_ref != null) { for (data.decls) |*d| { @@ -14533,6 +14680,20 @@ fn NewParser_( }, .s_function => |data| { + // We mark it as dead, but the value may not actually be dead + // We just want to be sure to not increment the usage counts for anything in the function + const mark_as_dead = data.func.flags.contains(.is_export) and p.options.features.replace_exports.count() > 0 and p.isExportToEliminate(data.func.name.?.ref.?); + const original_is_dead = p.is_control_flow_dead; + + if (mark_as_dead) { + p.is_control_flow_dead = true; + } + defer { + if (mark_as_dead) { + p.is_control_flow_dead = original_is_dead; + } + } + data.func = p.visitFunc(data.func, data.func.open_parens_loc); // Handle exporting this function from a namespace @@ -14547,9 +14708,13 @@ fn NewParser_( .name = p.loadNameFromRef(data.func.name.?.ref.?), .name_loc = data.func.name.?.loc, }, stmt.loc), p.e(E.Identifier{ .ref = data.func.name.?.ref.? }, data.func.name.?.loc), p.allocator)); - } else { - stmts.ensureUnusedCapacity(2) catch unreachable; - stmts.appendAssumeCapacity(stmt.*); + } else if (!mark_as_dead) { + stmts.append(stmt.*) catch unreachable; + } else if (mark_as_dead) { + const name = data.func.name.?.ref.?; + if (p.options.features.replace_exports.getPtr(p.loadNameFromRef(name))) |replacement| { + _ = p.injectReplacementExport(stmts, name, data.func.name.?.loc, replacement); + } } // stmts.appendAssumeCapacity( @@ -14563,6 +14728,18 @@ fn NewParser_( return; }, .s_class => |data| { + const mark_as_dead = data.is_export and p.options.features.replace_exports.count() > 0 and p.isExportToEliminate(data.class.class_name.?.ref.?); + const original_is_dead = p.is_control_flow_dead; + + if (mark_as_dead) { + p.is_control_flow_dead = true; + } + defer { + if (mark_as_dead) { + p.is_control_flow_dead = original_is_dead; + } + } + const shadow_ref = p.visitClass(stmt.loc, &data.class); // Remove the export flag inside a namespace @@ -14571,8 +14748,19 @@ fn NewParser_( data.is_export = false; } - // Lower class field syntax for browsers that don't support it - stmts.appendSlice(p.lowerClass(js_ast.StmtOrExpr{ .stmt = stmt.* }, shadow_ref)) catch unreachable; + const lowered = p.lowerClass(js_ast.StmtOrExpr{ .stmt = stmt.* }, shadow_ref); + + if (!mark_as_dead or was_export_inside_namespace) + // Lower class field syntax for browsers that don't support it + stmts.appendSlice(lowered) catch unreachable + else { + const ref = data.class.class_name.?.ref.?; + if (p.options.features.replace_exports.getPtr(p.loadNameFromRef(ref))) |replacement| { + if (p.injectReplacementExport(stmts, ref, data.class.class_name.?.loc, replacement)) { + p.is_control_flow_dead = original_is_dead; + } + } + } // Handle exporting this class from a namespace if (was_export_inside_namespace) { @@ -14754,6 +14942,151 @@ fn NewParser_( try stmts.append(stmt.*); } + fn isExportToEliminate(p: *P, ref: Ref) bool { + const symbol_name = p.loadNameFromRef(ref); + return p.options.features.replace_exports.contains(symbol_name); + } + + fn visitDecls(p: *P, decls: []G.Decl, comptime is_possibly_decl_to_remove: bool) usize { + var i: usize = 0; + const count = decls.len; + var j: usize = 0; + var out_decls = decls; + while (i < count) : (i += 1) { + p.visitBinding(decls[i].binding, null); + + if (decls[i].value != null) { + var val = decls[i].value.?; + const was_anonymous_named_expr = p.isAnonymousNamedExpr(val); + var replacement: ?*const RuntimeFeatures.ReplaceableExport = null; + + const prev_macro_call_count = p.macro_call_count; + const orig_dead = p.is_control_flow_dead; + if (comptime is_possibly_decl_to_remove) { + if (decls[i].binding.data == .b_identifier) { + if (p.options.features.replace_exports.getPtr(p.loadNameFromRef(decls[i].binding.data.b_identifier.ref))) |replacer| { + replacement = replacer; + if (replacer.* != .replace) { + p.is_control_flow_dead = true; + } + } + } + } + + decls[i].value = p.visitExpr(val); + + if (comptime is_possibly_decl_to_remove) { + p.is_control_flow_dead = orig_dead; + } + if (comptime is_possibly_decl_to_remove) { + if (decls[i].binding.data == .b_identifier) { + if (replacement) |ptr| { + if (!p.replaceDeclAndPossiblyRemove(&decls[i], ptr)) { + continue; + } + } + } + } + + p.visitDecl( + &decls[i], + was_anonymous_named_expr, + if (comptime allow_macros) + prev_macro_call_count != p.macro_call_count + else + false, + ); + } else if (comptime is_possibly_decl_to_remove) { + if (decls[i].binding.data == .b_identifier) { + if (p.options.features.replace_exports.getPtr(p.loadNameFromRef(decls[i].binding.data.b_identifier.ref))) |ptr| { + if (!p.replaceDeclAndPossiblyRemove(&decls[i], ptr)) { + p.visitDecl( + &decls[i], + false, + false, + ); + } else { + continue; + } + } + } + } + + if (comptime is_possibly_decl_to_remove) { + out_decls[j] = decls[i]; + j += 1; + } + } + + if (comptime is_possibly_decl_to_remove) { + return j; + } + + return decls.len; + } + + fn injectReplacementExport(p: *P, stmts: *StmtList, name_ref: Ref, loc: logger.Loc, replacement: *const RuntimeFeatures.ReplaceableExport) bool { + switch (replacement.*) { + .delete => return false, + .replace => |value| { + const count = stmts.items.len; + var decls = p.allocator.alloc(G.Decl, 1) catch unreachable; + + decls[0] = .{ .binding = p.b(B.Identifier{ .ref = name_ref }, loc), .value = value }; + var local = p.s( + S.Local{ + .is_export = true, + .decls = decls, + }, + loc, + ); + p.visitAndAppendStmt(stmts, &local) catch unreachable; + return count != stmts.items.len; + }, + .inject => |with| { + const count = stmts.items.len; + var decls = p.allocator.alloc(G.Decl, 1) catch unreachable; + decls[0] = .{ + .binding = p.b( + B.Identifier{ .ref = p.declareSymbol(.other, loc, with.name) catch unreachable }, + loc, + ), + .value = with.value, + }; + + var local = p.s( + S.Local{ + .is_export = true, + .decls = decls, + }, + loc, + ); + p.visitAndAppendStmt(stmts, &local) catch unreachable; + return count != stmts.items.len; + }, + } + } + + fn replaceDeclAndPossiblyRemove(p: *P, decl: *G.Decl, replacement: *const RuntimeFeatures.ReplaceableExport) bool { + switch (replacement.*) { + .delete => return false, + .replace => |value| { + decl.*.value = p.visitExpr(value); + return true; + }, + .inject => |with| { + decl.* = .{ + .binding = p.b( + B.Identifier{ .ref = p.declareSymbol(.other, decl.binding.loc, with.name) catch unreachable }, + decl.binding.loc, + ), + .value = p.visitExpr(Expr{ .data = with.value.data, .loc = if (decl.value != null) decl.value.?.loc else decl.binding.loc }), + }; + return true; + }, + } + } + fn visitBindingAndExprForMacro(p: *P, binding: Binding, expr: Expr) void { switch (binding.data) { .b_object => |bound_object| { @@ -15960,6 +16293,10 @@ fn NewParser_( var parts = _parts; // Insert an import statement for any runtime imports we generated + if (p.options.tree_shaking and p.options.features.trim_unused_imports) { + p.treeShake(&parts, false); + } + var parts_end: usize = 0; // Handle import paths after the whole file has been visited because we need // symbol usage counts to be able to remove unused type-only imports in |