diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index 960b7a2f3b..4e24cca1c8 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -19,19 +19,43 @@ src/ast/base.zig src/ast/Binding.zig src/ast/BundledAst.zig src/ast/CharFreq.zig +src/ast/ConvertESMExportsForHmr.zig src/ast/E.zig src/ast/Expr.zig +src/ast/foldStringAddition.zig src/ast/G.zig +src/ast/ImportScanner.zig +src/ast/KnownGlobal.zig src/ast/Macro.zig +src/ast/maybe.zig src/ast/NewStore.zig src/ast/Op.zig +src/ast/P.zig +src/ast/parse.zig +src/ast/parseFn.zig +src/ast/parseImportExport.zig +src/ast/parseJSXElement.zig +src/ast/parsePrefix.zig +src/ast/parseProperty.zig +src/ast/Parser.zig +src/ast/parseStmt.zig +src/ast/parseSuffix.zig +src/ast/parseTypescript.zig src/ast/S.zig src/ast/Scope.zig src/ast/ServerComponentBoundary.zig +src/ast/SideEffects.zig +src/ast/skipTypescript.zig src/ast/Stmt.zig src/ast/Symbol.zig +src/ast/symbols.zig src/ast/TS.zig +src/ast/TypeScript.zig src/ast/UseDirective.zig +src/ast/visit.zig +src/ast/visitBinaryExpression.zig +src/ast/visitExpr.zig +src/ast/visitStmt.zig src/async/posix_event_loop.zig src/async/stub_event_loop.zig src/async/windows_event_loop.zig diff --git a/scripts/sort-imports.ts b/scripts/sort-imports.ts index b83c34cdc1..1358b32f61 100755 --- a/scripts/sort-imports.ts +++ b/scripts/sort-imports.ts @@ -14,7 +14,7 @@ const usage = String.raw` /_____ \____/|__| |__| /_____ \__|__|_| / __/ \____/|__| |__| /____ > \/ \/ \/|__| \/ -Usage: bun scripts/sortImports [options] +Usage: bun scripts/sort-imports [options] Options: --help Show this help message @@ -22,7 +22,7 @@ Options: --keep-unused Don't remove unused imports Examples: - bun scripts/sortImports src + bun scripts/sort-imports src `.slice(1); if (args.includes("--help")) { console.log(usage); diff --git a/src/ast/ConvertESMExportsForHmr.zig b/src/ast/ConvertESMExportsForHmr.zig new file mode 100644 index 0000000000..561fdeb18c --- /dev/null +++ b/src/ast/ConvertESMExportsForHmr.zig @@ -0,0 +1,526 @@ +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.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); + }, + .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.push(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.fromList(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 = .init(&.{}), + }, .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.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; +} + +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; diff --git a/src/ast/ImportScanner.zig b/src/ast/ImportScanner.zig new file mode 100644 index 0000000000..5ab81b9a82 --- /dev/null +++ b/src/ast/ImportScanner.zig @@ -0,0 +1,530 @@ +stmts: []Stmt = &.{}, +kept_import_equals: bool = false, +removed_import_equals: bool = false, + +pub fn scan( + comptime P: type, + p: *P, + stmts: []Stmt, + will_transform_to_common_js: bool, + comptime hot_module_reloading_transformations: bool, + hot_module_reloading_context: if (hot_module_reloading_transformations) *ConvertESMExportsForHmr else void, +) !ImportScanner { + var scanner = ImportScanner{}; + var stmts_end: usize = 0; + const allocator = p.allocator; + const is_typescript_enabled: bool = comptime P.parser_features.typescript; + + for (stmts) |_stmt| { + var stmt = _stmt; // copy + switch (stmt.data) { + .s_import => |import_ptr| { + var st = import_ptr.*; + defer import_ptr.* = st; + + const record: *ImportRecord = &p.import_records.items[st.import_record_index]; + + if (record.path.isMacro()) { + record.is_unused = true; + record.path.is_disabled = true; + continue; + } + + // The official TypeScript compiler always removes unused imported + // symbols. However, we deliberately deviate from the official + // TypeScript compiler's behavior doing this in a specific scenario: + // we are not bundling, symbol renaming is off, and the tsconfig.json + // "importsNotUsedAsValues" setting is present and is not set to + // "remove". + // + // This exists to support the use case of compiling partial modules for + // compile-to-JavaScript languages such as Svelte. These languages try + // to reference imports in ways that are impossible for esbuild to know + // about when esbuild is only given a partial module to compile. Here + // is an example of some Svelte code that might use esbuild to convert + // TypeScript to JavaScript: + // + // + //
+ //

Hello {name}!

+ // + //
+ // + // Tools that use esbuild to compile TypeScript code inside a Svelte + // file like this only give esbuild the contents of the - //
- //

Hello {name}!

- // - //
- // - // Tools that use esbuild to compile TypeScript code inside a Svelte - // file like this only give esbuild the contents of the