mirror of
https://github.com/oven-sh/bun
synced 2026-02-11 19:38:58 +00:00
(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>
533 lines
20 KiB
Zig
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;
|