Files
bun.sh/src/ast/ConvertESMExportsForHmr.zig
taylor.fish edf13bd91d Refactor BabyList (#22502)
(For internal tracking: fixes STAB-1129, STAB-1145, STAB-1146,
STAB-1150, STAB-1126, STAB-1147, STAB-1148, STAB-1149, STAB-1158)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-09-09 20:41:10 -07:00

533 lines
20 KiB
Zig

last_part: *js_ast.Part,
// files in node modules will not get hot updates, so the code generation
// can be a bit more concise for re-exports
is_in_node_modules: bool,
imports_seen: bun.StringArrayHashMapUnmanaged(ImportRef) = .{},
export_star_props: std.ArrayListUnmanaged(G.Property) = .{},
export_props: std.ArrayListUnmanaged(G.Property) = .{},
stmts: std.ArrayListUnmanaged(Stmt) = .{},
const ImportRef = struct {
/// Index into ConvertESMExportsForHmr.stmts
stmt_index: u32,
};
pub fn convertStmt(ctx: *ConvertESMExportsForHmr, p: anytype, stmt: Stmt) !void {
const new_stmt = switch (stmt.data) {
else => brk: {
break :brk stmt;
},
.s_local => |st| stmt: {
if (!st.is_export) {
break :stmt stmt;
}
st.is_export = false;
var new_len: usize = 0;
for (st.decls.slice()) |*decl_ptr| {
const decl = decl_ptr.*; // explicit copy to avoid aliasinng
const value = decl.value orelse {
st.decls.mut(new_len).* = decl;
new_len += 1;
try ctx.visitBindingToExport(p, decl.binding);
continue;
};
switch (decl.binding.data) {
.b_missing => {},
.b_identifier => |id| {
const symbol = p.symbols.items[id.ref.inner_index];
// if the symbol is not used, we don't need to preserve
// a binding in this scope. we can move it to the exports object.
if (symbol.use_count_estimate == 0 and value.canBeMoved()) {
try ctx.export_props.append(p.allocator, .{
.key = Expr.init(E.String, .{ .data = symbol.original_name }, decl.binding.loc),
.value = value,
});
} else {
st.decls.mut(new_len).* = decl;
new_len += 1;
try ctx.visitBindingToExport(p, decl.binding);
}
},
else => {
st.decls.mut(new_len).* = decl;
new_len += 1;
try ctx.visitBindingToExport(p, decl.binding);
},
}
}
if (new_len == 0) {
return;
}
st.decls.len = @intCast(new_len);
break :stmt stmt;
},
.s_export_default => |st| stmt: {
// When React Fast Refresh needs to tag the default export, the statement
// cannot be moved, since a local reference is required.
if (p.options.features.react_fast_refresh and
st.value == .stmt and st.value.stmt.data == .s_function)
fast_refresh_edge_case: {
const symbol = st.value.stmt.data.s_function.func.name orelse
break :fast_refresh_edge_case;
const name = p.symbols.items[symbol.ref.?.inner_index].original_name;
if (ReactRefresh.isComponentishName(name)) {
// Lower to a function statement, and reference the function in the export list.
try ctx.export_props.append(p.allocator, .{
.key = Expr.init(E.String, .{ .data = "default" }, stmt.loc),
.value = Expr.initIdentifier(symbol.ref.?, stmt.loc),
});
break :stmt st.value.stmt;
}
// All other functions can be properly moved.
}
// Try to move the export default expression to the end.
const can_be_moved_to_inner_scope = switch (st.value) {
.stmt => |s| switch (s.data) {
.s_class => |c| c.class.canBeMoved() and (if (c.class.class_name) |name|
p.symbols.items[name.ref.?.inner_index].use_count_estimate == 0
else
true),
.s_function => |f| if (f.func.name) |name|
p.symbols.items[name.ref.?.inner_index].use_count_estimate == 0
else
true,
else => unreachable,
},
.expr => |e| switch (e.data) {
.e_identifier => true,
else => e.canBeMoved(),
},
};
if (can_be_moved_to_inner_scope) {
try ctx.export_props.append(p.allocator, .{
.key = Expr.init(E.String, .{ .data = "default" }, stmt.loc),
.value = st.value.toExpr(),
});
// no statement emitted
return;
}
// Otherwise, an identifier must be exported
switch (st.value) {
.expr => {
const temp_id = p.generateTempRef("default_export");
try ctx.last_part.declared_symbols.append(p.allocator, .{ .ref = temp_id, .is_top_level = true });
try ctx.last_part.symbol_uses.putNoClobber(p.allocator, temp_id, .{ .count_estimate = 1 });
try p.current_scope.generated.append(p.allocator, temp_id);
try ctx.export_props.append(p.allocator, .{
.key = Expr.init(E.String, .{ .data = "default" }, stmt.loc),
.value = Expr.initIdentifier(temp_id, stmt.loc),
});
break :stmt Stmt.alloc(S.Local, .{
.kind = .k_const,
.decls = try G.Decl.List.fromSlice(p.allocator, &.{
.{
.binding = Binding.alloc(p.allocator, B.Identifier{ .ref = temp_id }, stmt.loc),
.value = st.value.toExpr(),
},
}),
}, stmt.loc);
},
.stmt => |s| {
try ctx.export_props.append(p.allocator, .{
.key = Expr.init(E.String, .{ .data = "default" }, stmt.loc),
.value = Expr.initIdentifier(switch (s.data) {
.s_class => |class| class.class.class_name.?.ref.?,
.s_function => |func| func.func.name.?.ref.?,
else => unreachable,
}, stmt.loc),
});
break :stmt s;
},
}
},
.s_class => |st| stmt: {
// Strip the "export" keyword
if (!st.is_export) {
break :stmt stmt;
}
// Export as CommonJS
try ctx.export_props.append(p.allocator, .{
.key = Expr.init(E.String, .{
.data = p.symbols.items[st.class.class_name.?.ref.?.inner_index].original_name,
}, stmt.loc),
.value = Expr.initIdentifier(st.class.class_name.?.ref.?, stmt.loc),
});
st.is_export = false;
break :stmt stmt;
},
.s_function => |st| stmt: {
// Strip the "export" keyword
if (!st.func.flags.contains(.is_export)) break :stmt stmt;
st.func.flags.remove(.is_export);
try ctx.visitRefToExport(
p,
st.func.name.?.ref.?,
null,
stmt.loc,
false,
);
break :stmt stmt;
},
.s_export_clause => |st| {
for (st.items) |item| {
const ref = item.name.ref.?;
try ctx.visitRefToExport(p, ref, item.alias, item.name.loc, false);
}
return; // do not emit a statement here
},
.s_export_from => |st| {
const namespace_ref = try ctx.deduplicatedImport(
p,
st.import_record_index,
st.namespace_ref,
st.items,
stmt.loc,
null,
stmt.loc,
);
for (st.items) |*item| {
const ref = item.name.ref.?;
const symbol = &p.symbols.items[ref.innerIndex()];
if (symbol.namespace_alias == null) {
symbol.namespace_alias = .{
.namespace_ref = namespace_ref,
.alias = item.original_name,
.import_record_index = st.import_record_index,
};
}
try ctx.visitRefToExport(
p,
ref,
item.alias,
item.name.loc,
!ctx.is_in_node_modules, // live binding when this may be replaced
);
// imports and export statements have their alias +
// original_name swapped. this is likely a design bug in
// the parser but since everything uses these
// assumptions, this hack is simpler than making it
// proper
const alias = item.alias;
item.alias = item.original_name;
item.original_name = alias;
}
return;
},
.s_export_star => |st| {
const namespace_ref = try ctx.deduplicatedImport(
p,
st.import_record_index,
st.namespace_ref,
&.{},
stmt.loc,
null,
stmt.loc,
);
if (st.alias) |alias| {
// 'export * as ns from' creates one named property.
try ctx.export_props.append(p.allocator, .{
.key = Expr.init(E.String, .{ .data = alias.original_name }, stmt.loc),
.value = Expr.initIdentifier(namespace_ref, stmt.loc),
});
} else {
// 'export * from' creates a spread, hoisted at the top.
try ctx.export_star_props.append(p.allocator, .{
.kind = .spread,
.value = Expr.initIdentifier(namespace_ref, stmt.loc),
});
}
return;
},
// De-duplicate import statements. It is okay to disregard
// named/default imports here as we always rewrite them as
// full qualified property accesses (needed for live-bindings)
.s_import => |st| {
_ = try ctx.deduplicatedImport(
p,
st.import_record_index,
st.namespace_ref,
st.items,
st.star_name_loc,
st.default_name,
stmt.loc,
);
return;
},
};
try ctx.stmts.append(p.allocator, new_stmt);
}
/// Deduplicates imports, returning a previously used Ref if present.
fn deduplicatedImport(
ctx: *ConvertESMExportsForHmr,
p: anytype,
import_record_index: u32,
namespace_ref: Ref,
items: []js_ast.ClauseItem,
star_name_loc: ?logger.Loc,
default_name: ?js_ast.LocRef,
loc: logger.Loc,
) !Ref {
const ir = &p.import_records.items[import_record_index];
const gop = try ctx.imports_seen.getOrPut(p.allocator, ir.path.text);
if (gop.found_existing) {
// Disable this one since an older record is getting used. It isn't
// practical to delete this import record entry since an import or
// require expression can exist.
ir.is_unused = true;
const stmt = ctx.stmts.items[gop.value_ptr.stmt_index].data.s_import;
if (items.len > 0) {
if (stmt.items.len == 0) {
stmt.items = items;
} else {
stmt.items = try std.mem.concat(p.allocator, js_ast.ClauseItem, &.{ stmt.items, items });
}
}
if (namespace_ref.isValid()) {
if (!stmt.namespace_ref.isValid()) {
stmt.namespace_ref = namespace_ref;
return namespace_ref;
} else {
// Erase this namespace ref, but since it may be used in
// existing AST trees, a link must be established.
const symbol = &p.symbols.items[namespace_ref.innerIndex()];
symbol.use_count_estimate = 0;
symbol.link = stmt.namespace_ref;
if (@hasField(@typeInfo(@TypeOf(p)).pointer.child, "symbol_uses")) {
_ = p.symbol_uses.swapRemove(namespace_ref);
}
}
}
if (stmt.star_name_loc == null) if (star_name_loc) |stl| {
stmt.star_name_loc = stl;
};
if (stmt.default_name == null) if (default_name) |dn| {
stmt.default_name = dn;
};
return stmt.namespace_ref;
}
try ctx.stmts.append(p.allocator, Stmt.alloc(S.Import, .{
.import_record_index = import_record_index,
.is_single_line = true,
.default_name = default_name,
.items = items,
.namespace_ref = namespace_ref,
.star_name_loc = star_name_loc,
}, loc));
gop.value_ptr.* = .{ .stmt_index = @intCast(ctx.stmts.items.len - 1) };
return namespace_ref;
}
fn visitBindingToExport(ctx: *ConvertESMExportsForHmr, p: anytype, binding: Binding) !void {
switch (binding.data) {
.b_missing => {},
.b_identifier => |id| {
try ctx.visitRefToExport(p, id.ref, null, binding.loc, false);
},
.b_array => |array| {
for (array.items) |item| {
try ctx.visitBindingToExport(p, item.binding);
}
},
.b_object => |object| {
for (object.properties) |item| {
try ctx.visitBindingToExport(p, item.value);
}
},
}
}
fn visitRefToExport(
ctx: *ConvertESMExportsForHmr,
p: anytype,
ref: Ref,
export_symbol_name: ?[]const u8,
loc: logger.Loc,
is_live_binding_source: bool,
) !void {
const symbol = p.symbols.items[ref.inner_index];
const id = if (symbol.kind == .import)
Expr.init(E.ImportIdentifier, .{ .ref = ref }, loc)
else
Expr.initIdentifier(ref, loc);
if (is_live_binding_source or (symbol.kind == .import and !ctx.is_in_node_modules) or symbol.has_been_assigned_to) {
// TODO (2024-11-24) instead of requiring getters for live-bindings,
// a callback propagation system should be considered. mostly
// because here, these might not even be live bindings, and
// re-exports are so, so common.
//
// update(2025-03-05): HMRModule in ts now contains an exhaustive map
// of importers. For local live bindings, these can just remember to
// mutate the field in the exports object. Re-exports can just be
// encoded into the module format, propagated in `replaceModules`
const key = Expr.init(E.String, .{
.data = export_symbol_name orelse symbol.original_name,
}, loc);
// This is technically incorrect in that we've marked this as a
// top level symbol. but all we care about is preventing name
// collisions, not necessarily the best minificaiton (dev only)
const arg1 = p.generateTempRef(symbol.original_name);
try ctx.last_part.declared_symbols.append(p.allocator, .{ .ref = arg1, .is_top_level = true });
try ctx.last_part.symbol_uses.putNoClobber(p.allocator, arg1, .{ .count_estimate = 1 });
try p.current_scope.generated.append(p.allocator, arg1);
// 'get abc() { return abc }'
try ctx.export_props.append(p.allocator, .{
.kind = .get,
.key = key,
.value = Expr.init(E.Function, .{ .func = .{
.body = .{
.stmts = try p.allocator.dupe(Stmt, &.{
Stmt.alloc(S.Return, .{ .value = id }, loc),
}),
.loc = loc,
},
} }, loc),
});
// no setter is added since live bindings are read-only
} else {
// 'abc,'
try ctx.export_props.append(p.allocator, .{
.key = Expr.init(E.String, .{
.data = export_symbol_name orelse symbol.original_name,
}, loc),
.value = id,
});
}
}
pub fn finalize(ctx: *ConvertESMExportsForHmr, p: anytype, all_parts: []js_ast.Part) !void {
if (ctx.export_star_props.items.len > 0) {
if (ctx.export_props.items.len == 0) {
ctx.export_props = ctx.export_star_props;
} else {
const export_star_len = ctx.export_star_props.items.len;
try ctx.export_props.ensureUnusedCapacity(p.allocator, export_star_len);
const len = ctx.export_props.items.len;
ctx.export_props.items.len += export_star_len;
bun.copy(G.Property, ctx.export_props.items[export_star_len..], ctx.export_props.items[0..len]);
@memcpy(ctx.export_props.items[0..export_star_len], ctx.export_star_props.items);
}
}
if (ctx.export_props.items.len > 0) {
const obj = Expr.init(E.Object, .{
.properties = G.Property.List.moveFromList(&ctx.export_props),
}, logger.Loc.Empty);
// `hmr.exports = ...`
try ctx.stmts.append(p.allocator, Stmt.alloc(S.SExpr, .{
.value = Expr.assign(
Expr.init(E.Dot, .{
.target = Expr.initIdentifier(p.hmr_api_ref, logger.Loc.Empty),
.name = "exports",
.name_loc = logger.Loc.Empty,
}, logger.Loc.Empty),
obj,
),
}, logger.Loc.Empty));
// mark a dependency on module_ref so it is renamed
try ctx.last_part.symbol_uses.put(p.allocator, p.module_ref, .{ .count_estimate = 1 });
try ctx.last_part.declared_symbols.append(p.allocator, .{ .ref = p.module_ref, .is_top_level = true });
}
if (p.options.features.react_fast_refresh and p.react_refresh.register_used) {
try ctx.stmts.append(p.allocator, Stmt.alloc(S.SExpr, .{
.value = Expr.init(E.Call, .{
.target = Expr.init(E.Dot, .{
.target = Expr.initIdentifier(p.hmr_api_ref, .Empty),
.name = "reactRefreshAccept",
.name_loc = .Empty,
}, .Empty),
.args = .empty,
}, .Empty),
}, .Empty));
}
// Merge all part metadata into the first part.
for (all_parts[0 .. all_parts.len - 1]) |*part| {
try ctx.last_part.declared_symbols.appendList(p.allocator, part.declared_symbols);
try ctx.last_part.import_record_indices.appendSlice(
p.allocator,
part.import_record_indices.slice(),
);
for (part.symbol_uses.keys(), part.symbol_uses.values()) |k, v| {
const gop = try ctx.last_part.symbol_uses.getOrPut(p.allocator, k);
if (!gop.found_existing) {
gop.value_ptr.* = v;
} else {
gop.value_ptr.count_estimate += v.count_estimate;
}
}
part.stmts = &.{};
part.declared_symbols.entries.len = 0;
part.tag = .dead_due_to_inlining;
part.dependencies.clearRetainingCapacity();
try part.dependencies.append(p.allocator, .{
.part_index = @intCast(all_parts.len - 1),
.source_index = p.source.index,
});
}
try ctx.last_part.import_record_indices.appendSlice(
p.allocator,
p.import_records_for_current_part.items,
);
try ctx.last_part.declared_symbols.appendList(p.allocator, p.declared_symbols);
ctx.last_part.stmts = ctx.stmts.items;
ctx.last_part.tag = .none;
}
const bun = @import("bun");
const logger = bun.logger;
const js_ast = bun.ast;
const B = js_ast.B;
const Binding = js_ast.Binding;
const E = js_ast.E;
const Expr = js_ast.Expr;
const LocRef = js_ast.LocRef;
const S = js_ast.S;
const Stmt = js_ast.Stmt;
const G = js_ast.G;
const Decl = G.Decl;
const Property = G.Property;
const js_parser = bun.js_parser;
const ConvertESMExportsForHmr = js_parser.ConvertESMExportsForHmr;
const ReactRefresh = js_parser.ReactRefresh;
const Ref = js_parser.Ref;
const options = js_parser.options;
const std = @import("std");
const List = std.ArrayListUnmanaged;