kit: implement server components dev server (#14025)

This commit is contained in:
dave caruso
2024-09-27 20:53:39 -07:00
committed by GitHub
parent d09df1af47
commit 514d37b3d2
82 changed files with 3812 additions and 5964 deletions

View File

@@ -1061,7 +1061,7 @@ pub const ImportScanner = struct {
stmts: []Stmt,
will_transform_to_common_js: bool,
comptime hot_module_reloading_transformations: bool,
hot_module_reloading_context: if (hot_module_reloading_transformations) *P.ConvertESMExportsForHmr else void,
hot_module_reloading_context: if (hot_module_reloading_transformations) *ConvertESMExportsForHmr else void,
) !ImportScanner {
var scanner = ImportScanner{};
var stmts_end: usize = 0;
@@ -1077,7 +1077,7 @@ pub const ImportScanner = struct {
st__.* = st;
}
var record: *ImportRecord = &p.import_records.items[st.import_record_index];
const record: *ImportRecord = &p.import_records.items[st.import_record_index];
if (record.path.isMacro()) {
record.is_unused = true;
@@ -1272,7 +1272,7 @@ pub const ImportScanner = struct {
result.* = alias;
}
strings.sortDesc(sorted);
p.named_imports.ensureUnusedCapacity(sorted.len) catch unreachable;
p.named_imports.ensureUnusedCapacity(p.allocator, sorted.len) catch bun.outOfMemory();
// Create named imports for these property accesses. This will
// cause missing imports to generate useful warnings.
@@ -1283,6 +1283,7 @@ pub const ImportScanner = struct {
for (sorted) |alias| {
const item = existing_items.get(alias).?;
p.named_imports.put(
p.allocator,
item.ref.?,
js_ast.NamedImport{
.alias = alias,
@@ -1290,7 +1291,7 @@ pub const ImportScanner = struct {
.namespace_ref = namespace_ref,
.import_record_index = st.import_record_index,
},
) catch unreachable;
) catch bun.outOfMemory();
const name: LocRef = item;
const name_ref = name.ref.?;
@@ -1314,8 +1315,9 @@ pub const ImportScanner = struct {
}
p.named_imports.ensureUnusedCapacity(
p.allocator,
st.items.len + @as(usize, @intFromBool(st.default_name != null)) + @as(usize, @intFromBool(st.star_name_loc != null)),
) catch unreachable;
) catch bun.outOfMemory();
if (st.star_name_loc) |loc| {
p.named_imports.putAssumeCapacity(
@@ -1370,7 +1372,7 @@ pub const ImportScanner = struct {
const name: LocRef = item.name;
const name_ref = name.ref.?;
try p.named_imports.put(name_ref, js_ast.NamedImport{
try p.named_imports.put(p.allocator, name_ref, js_ast.NamedImport{
.alias = item.alias,
.alias_loc = name.loc,
.namespace_ref = namespace_ref,
@@ -1486,9 +1488,9 @@ pub const ImportScanner = struct {
// Rewrite this export to be:
// exports.default =
// But only if it's anonymous
if (!hot_module_reloading_transformations and will_transform_to_common_js) {
if (!hot_module_reloading_transformations and will_transform_to_common_js and P != bun.bundle_v2.AstBuilder) {
const expr = st.value.toExpr();
var export_default_args = p.allocator.alloc(Expr, 2) catch unreachable;
var export_default_args = try p.allocator.alloc(Expr, 2);
export_default_args[0] = p.@"module.exports"(expr.loc);
export_default_args[1] = expr;
stmt = p.s(S.SExpr{ .value = p.callRuntime(expr.loc, "__exportDefault", export_default_args) }, expr.loc);
@@ -1504,7 +1506,7 @@ pub const ImportScanner = struct {
if (st.alias) |alias| {
// "export * as ns from 'path'"
try p.named_imports.put(st.namespace_ref, js_ast.NamedImport{
try p.named_imports.put(p.allocator, st.namespace_ref, js_ast.NamedImport{
.alias = null,
.alias_is_star = true,
.alias_loc = alias.loc,
@@ -1522,13 +1524,13 @@ pub const ImportScanner = struct {
},
.s_export_from => |st| {
try p.import_records_for_current_part.append(allocator, st.import_record_index);
p.named_imports.ensureUnusedCapacity(st.items.len) catch unreachable;
p.named_imports.ensureUnusedCapacity(p.allocator, st.items.len) catch unreachable;
for (st.items) |item| {
const ref = item.name.ref orelse p.panic("Expected export from item to have a name {any}", .{st});
// Note that the imported alias is not item.Alias, which is the
// exported alias. This is somewhat confusing because each
// SExportFrom statement is basically SImport + SExportClause in one.
try p.named_imports.put(ref, js_ast.NamedImport{
try p.named_imports.put(p.allocator, ref, js_ast.NamedImport{
.alias_is_star = false,
.alias = item.original_name,
.alias_loc = item.name.loc,
@@ -2842,7 +2844,7 @@ pub const ScanPassResult = struct {
pub fn init(allocator: Allocator) ScanPassResult {
return .{
.import_records = ListManaged(ImportRecord).init(allocator),
.named_imports = js_ast.Ast.NamedImports.init(allocator),
.named_imports = .{},
.used_symbols = ParsePassSymbolUsageMap.init(allocator),
.import_records_to_keep = ListManaged(u32).init(allocator),
.approximate_newline_count = 0,
@@ -3715,7 +3717,6 @@ pub const Parser = struct {
part.symbol_uses = .{};
return js_ast.Result{
.ast = js_ast.Ast{
.allocator = p.allocator,
.import_records = ImportRecord.List.init(p.import_records.items),
.redirect_import_record_index = id,
.named_imports = p.named_imports,
@@ -4421,7 +4422,7 @@ const ParserFeatures = struct {
scan_only: bool = false,
};
const ImportItemForNamespaceMap = bun.StringArrayHashMap(LocRef);
pub const ImportItemForNamespaceMap = bun.StringArrayHashMap(LocRef);
pub const KnownGlobal = enum {
WeakSet,
@@ -5797,7 +5798,7 @@ fn NewParser_(
}
}
pub fn recordExport(p: *P, loc: logger.Loc, alias: string, ref: Ref) anyerror!void {
pub fn recordExport(p: *P, loc: logger.Loc, alias: string, ref: Ref) !void {
if (p.named_exports.get(alias)) |name| {
// Duplicate exports are an error
var notes = try p.allocator.alloc(logger.Data, 1);
@@ -5814,7 +5815,7 @@ fn NewParser_(
.{std.mem.trim(u8, alias, "\"'")},
);
} else if (!p.isDeoptimizedCommonJS()) {
try p.named_exports.put(alias, js_ast.NamedExport{ .alias_loc = loc, .ref = ref });
try p.named_exports.put(p.allocator, alias, js_ast.NamedExport{ .alias_loc = loc, .ref = ref });
}
}
@@ -5865,7 +5866,6 @@ fn NewParser_(
},
.e_private_identifier => |private| {
return p.loadNameFromRef(private.ref);
// return p.loadNameFromRef()
},
else => {
return "property";
@@ -6052,7 +6052,7 @@ fn NewParser_(
};
declared_symbols.appendAssumeCapacity(.{ .ref = ref, .is_top_level = true });
try p.is_import_item.put(allocator, ref, {});
try p.named_imports.put(ref, js_ast.NamedImport{
try p.named_imports.put(allocator, ref, js_ast.NamedImport{
.alias = alias_name,
.alias_loc = logger.Loc{},
.namespace_ref = namespace_ref,
@@ -6147,7 +6147,7 @@ fn NewParser_(
declared_symbols.appendAssumeCapacity(.{ .ref = entry.ref, .is_top_level = true });
try p.module_scope.generated.push(allocator, entry.ref);
try p.is_import_item.put(allocator, entry.ref, {});
try p.named_imports.put(entry.ref, .{
try p.named_imports.put(allocator, entry.ref, .{
.alias = entry.name,
.alias_loc = logger.Loc.Empty,
.namespace_ref = namespace_ref,
@@ -9200,6 +9200,8 @@ fn NewParser_(
}
}
}
} else if (import_tag == .kit_resolve_to_ssr_graph) {
p.import_records.items[stmt.import_record_index].tag = import_tag;
}
}
@@ -10206,7 +10208,7 @@ fn NewParser_(
isForAwait = false;
} else {
// TODO: improve error handling here
// didGenerateError := p.markSyntaxFeature(compat.ForAwait, awaitRange)
// didGenerateError := p.markSyntaxFeature(compat.ForAwait, awaitRange)
if (p.fn_or_arrow_data_parse.is_top_level) {
p.top_level_await_keyword = await_range;
// p.markSyntaxFeature(compat.TopLevelAwait, awaitRange)
@@ -12209,6 +12211,7 @@ fn NewParser_(
const SupportedAttribute = enum {
type,
embed,
bunKitGraph,
};
var has_seen_embed_true = false;
@@ -12217,21 +12220,17 @@ fn NewParser_(
const supported_attribute: ?SupportedAttribute = brk: {
// Parse the key
if (p.lexer.isIdentifierOrKeyword()) {
if (strings.eqlComptime(p.lexer.identifier, "type")) {
break :brk .type;
}
if (strings.eqlComptime(p.lexer.identifier, "embed")) {
break :brk .embed;
inline for (comptime std.enums.values(SupportedAttribute)) |t| {
if (strings.eqlComptime(p.lexer.identifier, @tagName(t))) {
break :brk t;
}
}
} else if (p.lexer.token == .t_string_literal) {
if (p.lexer.string_literal_is_ascii) {
if (strings.eqlComptime(p.lexer.string_literal_slice, "type")) {
break :brk .type;
}
if (strings.eqlComptime(p.lexer.string_literal_slice, "embed")) {
break :brk .embed;
inline for (comptime std.enums.values(SupportedAttribute)) |t| {
if (strings.eqlComptime(p.lexer.string_literal_slice, @tagName(t))) {
break :brk t;
}
}
}
} else {
@@ -12275,6 +12274,13 @@ fn NewParser_(
}
}
},
.bunKitGraph => {
if (strings.eqlComptime(p.lexer.string_literal_slice, "ssr")) {
path.import_tag = .kit_resolve_to_ssr_graph;
} else {
try p.lexer.addRangeError(p.lexer.range(), "'bunKitGraph' can only be set to 'ssr'", .{}, true);
}
},
}
}
}
@@ -16225,10 +16231,10 @@ fn NewParser_(
return exp;
}
// // Capture "this" inside arrow functions that will be lowered into normal
// // Capture "this" inside arrow functions that will be lowered into normal
// // function expressions for older language environments
// if p.fnOrArrowDataVisit.isArrow && p.options.unsupportedJSFeatures.Has(compat.Arrow) && p.fnOnlyDataVisit.isThisNested {
// return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EIdentifier{Ref: p.captureThis()}}, exprOut{}
// return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EIdentifier{Ref: p.captureThis()}}, exprOut{}
// }
},
.e_import_meta => {
@@ -19931,7 +19937,7 @@ fn NewParser_(
data.cases[i].value = p.visitExpr(val);
// TODO: error messages
// Check("case", *c.Value, c.Value.Loc)
// p.warnAboutTypeofAndString(s.Test, *c.Value)
// p.warnAboutTypeofAndString(s.Test, *c.Value)
}
var _stmts = ListManaged(Stmt).fromOwnedSlice(p.allocator, case.body);
p.visitStmts(&_stmts, StmtsKind.none) catch unreachable;
@@ -23488,15 +23494,14 @@ fn NewParser_(
parts_list.cap = @intCast(input_parts.len);
return .{
.allocator = p.allocator,
.runtime_imports = p.runtime_imports,
.parts = parts_list,
.module_scope = p.module_scope.*,
.symbols = js_ast.Symbol.List.init(p.symbols.items),
.symbols = js_ast.Symbol.List.fromList(p.symbols),
.exports_ref = p.exports_ref,
.wrapper_ref = wrapper_ref,
.module_ref = p.module_ref,
.import_records = ImportRecord.List.init(p.import_records.items),
.import_records = ImportRecord.List.fromList(p.import_records),
.export_star_import_records = p.export_star_import_records.items,
.approximate_newline_count = p.lexer.approximate_newline_count,
.exports_kind = exports_kind,
@@ -23588,317 +23593,6 @@ fn NewParser_(
return false;
}
const ConvertESMExportsForHmr = struct {
last_part: *js_ast.Part,
imports_seen: std.AutoArrayHashMapUnmanaged(u32, void) = .{},
export_props: std.ArrayListUnmanaged(G.Property) = .{},
stmts: std.ArrayListUnmanaged(Stmt) = .{},
fn convertStmt(ctx: *ConvertESMExportsForHmr, p: *P, stmt: Stmt) !void {
const new_stmt = switch (stmt.data) {
else => stmt,
.s_local => |st| stmt: {
if (!st.is_export) break :stmt stmt;
st.is_export = false;
if (st.kind.isReassignable()) {
for (st.decls.slice()) |decl| {
try ctx.visitBindingForKitModuleExports(p, decl.binding, true);
}
} else {
// TODO: remove this dupe
var dupe_decls = try std.ArrayListUnmanaged(G.Decl).initCapacity(p.allocator, st.decls.len);
for (st.decls.slice()) |decl| {
bun.assert(decl.value != null); // const must be initialized
switch (decl.binding.data) {
.b_missing => @panic("binding 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 or !decl.value.?.canBeMoved()) {
dupe_decls.appendAssumeCapacity(decl);
}
try ctx.export_props.append(p.allocator, .{
.key = Expr.init(E.String, .{ .data = symbol.original_name }, decl.binding.loc),
.value = decl.value,
});
},
else => {
dupe_decls.appendAssumeCapacity(decl);
try ctx.visitBindingForKitModuleExports(p, decl.binding, false);
},
}
}
if (dupe_decls.items.len == 0) {
return;
}
st.decls = G.Decl.List.fromList(dupe_decls);
}
break :stmt stmt;
},
.s_export_default => |st| stmt: {
// Simple case: we can move this to the default property of the exports object
if (st.canBeMoved()) {
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, we need a temporary
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.module_scope.generated.push(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);
},
.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);
// Export as CommonJS
try ctx.export_props.append(p.allocator, .{
.key = Expr.init(E.String, .{
.data = p.symbols.items[st.func.name.?.ref.?.inner_index].original_name,
}, stmt.loc),
.value = Expr.initIdentifier(st.func.name.?.ref.?, stmt.loc),
});
break :stmt stmt;
},
.s_export_clause => |st| {
for (st.items) |item| {
try ctx.export_props.append(p.allocator, .{
.key = Expr.init(E.String, .{
.data = item.alias,
}, stmt.loc),
.value = Expr.initIdentifier(item.name.ref.?, item.name.loc),
});
}
return; // do not emit a statement here
},
.s_export_from => |st| {
_ = st; // autofix
@panic("TODO s_export_from");
},
.s_export_star => |st| {
_ = st; // autofix
@panic("TODO s_export_star");
},
// De-duplicate import statements. It is okay to disregard
// named/default imports here as we always rewrite them as
// full qualified property accesses (need to so live-bindings)
.s_import => |st| stmt: {
const gop = try ctx.imports_seen.getOrPut(p.allocator, st.import_record_index);
if (gop.found_existing) return;
break :stmt stmt;
},
};
try ctx.stmts.append(p.allocator, new_stmt);
}
fn visitBindingForKitModuleExports(
ctx: *ConvertESMExportsForHmr,
p: *P,
binding: Binding,
is_live_binding: bool,
) !void {
switch (binding.data) {
.b_missing => @panic("missing!"),
.b_identifier => |id| {
try ctx.visitRefForKitModuleExports(p, id.ref, binding.loc, is_live_binding);
},
.b_array => |array| {
for (array.items) |item| {
try ctx.visitBindingForKitModuleExports(p, item.binding, is_live_binding);
}
},
.b_object => |object| {
for (object.properties) |item| {
try ctx.visitBindingForKitModuleExports(p, item.value, is_live_binding);
}
},
}
}
fn visitRefForKitModuleExports(
ctx: *ConvertESMExportsForHmr,
p: *P,
ref: Ref,
loc: logger.Loc,
is_live_binding: bool,
) !void {
const symbol = p.symbols.items[ref.inner_index];
const id = Expr.initIdentifier(ref, loc);
if (is_live_binding) {
const key = Expr.init(E.String, .{
.data = 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.module_scope.generated.push(p.allocator, arg1);
// Live bindings need to update the value internally and externally.
// '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),
});
// 'set abc(abc2) { abc = abc2 }'
try ctx.export_props.append(p.allocator, .{
.kind = .set,
.key = key,
.value = Expr.init(E.Function, .{ .func = .{
.args = try p.allocator.dupe(G.Arg, &.{.{
.binding = Binding.alloc(p.allocator, B.Identifier{ .ref = arg1 }, loc),
}}),
.body = .{
.stmts = try p.allocator.dupe(Stmt, &.{
Stmt.alloc(S.SExpr, .{
.value = Expr.assign(id, Expr.initIdentifier(arg1, loc)),
}, loc),
}),
.loc = loc,
},
} }, loc),
});
} else {
// 'abc,'
try ctx.export_props.append(p.allocator, .{
.key = Expr.init(E.String, .{
.data = symbol.original_name,
}, loc),
.value = id,
});
}
}
pub fn finalize(ctx: *ConvertESMExportsForHmr, p: *P, all_parts: []js_ast.Part) ![]js_ast.Part {
if (ctx.export_props.items.len > 0) {
// add a marker for the client runtime to tell that this is an ES module
try ctx.stmts.append(p.allocator, Stmt.alloc(S.SExpr, .{
.value = Expr.assign(
Expr.init(E.Dot, .{
.target = Expr.initIdentifier(p.module_ref, logger.Loc.Empty),
.name = "__esModule",
.name_loc = logger.Loc.Empty,
}, logger.Loc.Empty),
Expr.init(E.Boolean, .{ .value = true }, logger.Loc.Empty),
),
}, logger.Loc.Empty));
try ctx.stmts.append(p.allocator, Stmt.alloc(S.SExpr, .{
.value = Expr.assign(
Expr.init(E.Dot, .{
.target = Expr.initIdentifier(p.module_ref, logger.Loc.Empty),
.name = "exports",
.name_loc = logger.Loc.Empty,
}, logger.Loc.Empty),
Expr.init(E.Object, .{
.properties = G.Property.List.fromList(ctx.export_props),
}, logger.Loc.Empty),
),
}, 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 });
}
// TODO: this is a tiny mess. it is honestly trying to hard to merge all parts into one
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.append(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.push(p.allocator, .{
.part_index = @intCast(all_parts.len - 1),
.source_index = p.source.index,
});
}
try ctx.last_part.import_record_indices.append(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;
return all_parts;
}
};
pub fn init(
allocator: Allocator,
log: *logger.Log,
@@ -23933,7 +23627,7 @@ fn NewParser_(
.define = define,
.import_records = undefined,
.named_imports = undefined,
.named_exports = js_ast.Ast.NamedExports.init(allocator),
.named_exports = .{},
.log = log,
.allocator = allocator,
.options = opts,
@@ -23979,7 +23673,7 @@ fn NewParser_(
if (comptime !only_scan_imports_and_do_not_visit) {
this.import_records = @TypeOf(this.import_records).init(allocator);
this.named_imports = NamedImportsType.init(allocator);
this.named_imports = .{};
}
this.to_expr_wrapper_namespace = Binding2ExprWrapper.Namespace.init(this);
@@ -24095,6 +23789,316 @@ const WrapMode = enum {
bun_commonjs,
};
pub const ConvertESMExportsForHmr = struct {
last_part: *js_ast.Part,
imports_seen: std.AutoArrayHashMapUnmanaged(u32, void) = .{},
export_props: std.ArrayListUnmanaged(G.Property) = .{},
stmts: std.ArrayListUnmanaged(Stmt) = .{},
fn convertStmt(ctx: *ConvertESMExportsForHmr, p: anytype, stmt: Stmt) !void {
const new_stmt = switch (stmt.data) {
else => stmt,
.s_local => |st| stmt: {
if (!st.is_export) break :stmt stmt;
st.is_export = false;
if (st.kind.isReassignable()) {
for (st.decls.slice()) |decl| {
try ctx.visitBindingForKitModuleExports(p, decl.binding, true);
}
} else {
// TODO: remove this dupe
var dupe_decls = try std.ArrayListUnmanaged(G.Decl).initCapacity(p.allocator, st.decls.len);
for (st.decls.slice()) |decl| {
bun.assert(decl.value != null); // const must be initialized
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 decl.value.?.canBeMoved()) {
try ctx.export_props.append(p.allocator, .{
.key = Expr.init(E.String, .{ .data = symbol.original_name }, decl.binding.loc),
.value = decl.value,
});
} else {
dupe_decls.appendAssumeCapacity(decl);
try ctx.visitBindingForKitModuleExports(p, decl.binding, false);
}
},
else => {
dupe_decls.appendAssumeCapacity(decl);
try ctx.visitBindingForKitModuleExports(p, decl.binding, false);
},
}
}
if (dupe_decls.items.len == 0) {
return;
}
st.decls = G.Decl.List.fromList(dupe_decls);
}
break :stmt stmt;
},
.s_export_default => |st| stmt: {
// Simple case: we can move this to the default property of the exports object
if (st.canBeMoved()) {
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, we need a temporary
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.push(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);
},
.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);
// Export as CommonJS
try ctx.export_props.append(p.allocator, .{
.key = Expr.init(E.String, .{
.data = p.symbols.items[st.func.name.?.ref.?.inner_index].original_name,
}, stmt.loc),
.value = Expr.initIdentifier(st.func.name.?.ref.?, stmt.loc),
});
break :stmt stmt;
},
.s_export_clause => |st| {
for (st.items) |item| {
try ctx.export_props.append(p.allocator, .{
.key = Expr.init(E.String, .{
.data = item.alias,
}, stmt.loc),
.value = Expr.initIdentifier(item.name.ref.?, item.name.loc),
});
}
return; // do not emit a statement here
},
.s_export_from => {
bun.todoPanic(@src(), "hot-module-reloading instrumentation for 'export {{ ... }} from'", .{});
},
.s_export_star => {
bun.todoPanic(@src(), "hot-module-reloading instrumentation for 'export * from'", .{});
},
// De-duplicate import statements. It is okay to disregard
// named/default imports here as we always rewrite them as
// full qualified property accesses (need to so live-bindings)
.s_import => |st| stmt: {
const gop = try ctx.imports_seen.getOrPut(p.allocator, st.import_record_index);
if (gop.found_existing) return;
break :stmt stmt;
},
};
try ctx.stmts.append(p.allocator, new_stmt);
}
fn visitBindingForKitModuleExports(
ctx: *ConvertESMExportsForHmr,
p: anytype,
binding: Binding,
is_live_binding: bool,
) !void {
switch (binding.data) {
.b_missing => {},
.b_identifier => |id| {
try ctx.visitRefForKitModuleExports(p, id.ref, binding.loc, is_live_binding);
},
.b_array => |array| {
for (array.items) |item| {
try ctx.visitBindingForKitModuleExports(p, item.binding, is_live_binding);
}
},
.b_object => |object| {
for (object.properties) |item| {
try ctx.visitBindingForKitModuleExports(p, item.value, is_live_binding);
}
},
}
}
fn visitRefForKitModuleExports(
ctx: *ConvertESMExportsForHmr,
p: anytype,
ref: Ref,
loc: logger.Loc,
is_live_binding: bool,
) !void {
const symbol = p.symbols.items[ref.inner_index];
const id = Expr.initIdentifier(ref, loc);
if (is_live_binding) {
const key = Expr.init(E.String, .{
.data = 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.push(p.allocator, arg1);
// Live bindings need to update the value internally and externally.
// '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),
});
// 'set abc(abc2) { abc = abc2 }'
try ctx.export_props.append(p.allocator, .{
.kind = .set,
.key = key,
.value = Expr.init(E.Function, .{ .func = .{
.args = try p.allocator.dupe(G.Arg, &.{.{
.binding = Binding.alloc(p.allocator, B.Identifier{ .ref = arg1 }, loc),
}}),
.body = .{
.stmts = try p.allocator.dupe(Stmt, &.{
Stmt.alloc(S.SExpr, .{
.value = Expr.assign(id, Expr.initIdentifier(arg1, loc)),
}, loc),
}),
.loc = loc,
},
} }, loc),
});
} else {
// 'abc,'
try ctx.export_props.append(p.allocator, .{
.key = Expr.init(E.String, .{
.data = symbol.original_name,
}, loc),
.value = id,
});
}
}
pub fn finalize(ctx: *ConvertESMExportsForHmr, p: anytype, all_parts: []js_ast.Part) ![]js_ast.Part {
if (ctx.export_props.items.len > 0) {
// add a marker for the client runtime to tell that this is an ES module
try ctx.stmts.append(p.allocator, Stmt.alloc(S.SExpr, .{
.value = Expr.assign(
Expr.init(E.Dot, .{
.target = Expr.initIdentifier(p.module_ref, logger.Loc.Empty),
.name = "__esModule",
.name_loc = logger.Loc.Empty,
}, logger.Loc.Empty),
Expr.init(E.Boolean, .{ .value = true }, logger.Loc.Empty),
),
}, logger.Loc.Empty));
try ctx.stmts.append(p.allocator, Stmt.alloc(S.SExpr, .{
.value = Expr.assign(
Expr.init(E.Dot, .{
.target = Expr.initIdentifier(p.module_ref, logger.Loc.Empty),
.name = "exports",
.name_loc = logger.Loc.Empty,
}, logger.Loc.Empty),
Expr.init(E.Object, .{
.properties = G.Property.List.fromList(ctx.export_props),
}, logger.Loc.Empty),
),
}, 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 });
}
// TODO: this is a tiny mess. it is honestly trying to hard to merge all parts into one
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.append(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.push(p.allocator, .{
.part_index = @intCast(all_parts.len - 1),
.source_index = p.source.index,
});
}
try ctx.last_part.import_record_indices.append(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;
return all_parts;
}
};
/// Equivalent of esbuild's js_ast_helpers.ToInt32
fn floatToInt32(f: f64) i32 {
// Special-case non-finite numbers