Compare commits

...

13 Commits

Author SHA1 Message Date
autofix-ci[bot]
8d4fcd0164 [autofix.ci] apply automated fixes 2025-09-13 06:37:52 +00:00
Claude Bot
b1128d1d19 Complete implementation of --global-name with full nested support
Now properly handles:
- Simple identifiers: 'MyLib' -> 'var MyLib = (() => {})();'
- 2-part dot expressions: 'window.MyLib' -> 'var window; (window ||= {}).MyLib = (() => {})();'
- Multi-part expressions: 'window.my.deep.module' -> 'var window; (((window ||= {}).my ||= {}).deep ||= {}).module = (() => {})();'

Output exactly matches esbuild's implementation for all cases.
2025-09-13 06:35:43 +00:00
Claude Bot
16f38848bf Implement proper dot expression support for --global-name
Based on esbuild's implementation, now properly handles:
- Simple identifiers: 'MyLib' -> 'var MyLib = (() => {})();'
- 2-part dot expressions: 'window.MyLib' -> 'var window; (window ||= {}).MyLib = (() => {})();'

This matches esbuild's output exactly for these cases.
Multi-part expressions (3+ parts) still need full implementation.
2025-09-13 05:43:53 +00:00
Claude Bot
3418180e8e Revert to simple global name handling due to memory issues
The --global-name flag now works for simple identifiers like 'MyLib'.
Dot expressions like 'window.MyLib' are accepted but generate invalid JS.

Full dot expression support requires more careful memory management
to avoid use-after-free issues when parsing the global name.
2025-09-13 02:57:59 +00:00
Claude Bot
c6849740a7 Fix memory issues - revert to simple global name handling for now
The dot expression support needs more work to properly generate:
- var window; for the declaration
- (window ||= {}).MyLib = (() => {})(); for the assignment

Currently it just outputs 'var window.MyLib' which is invalid JavaScript.
2025-09-13 01:23:03 +00:00
Claude Bot
c9c8c9abfd Merge main into claude/implement-globalname-iife-support - resolved conflicts 2025-09-12 09:28:47 +00:00
Jarred Sumner
53a6d40c4b Delete test_entry.js 2025-08-30 03:10:30 -07:00
autofix-ci[bot]
3b75ea5790 [autofix.ci] apply automated fixes 2025-08-30 10:04:09 +00:00
Claude Bot
76d3175da6 fix: properly parse globalName in JS bundler and validate CLI args
- Add parsing for globalName (camelCase) and global_name (snake_case) properties in JSBundler Config.fromJS
- Fix memory leak by adding global_name.deinit() call in Config.deinit
- Restrict --global-name CLI flag to only work with --format=iife
- Add JavaScript identifier validation for --global-name using js_lexer.isIdentifier
- Return clear error messages for invalid usage

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-30 10:01:49 +00:00
autofix-ci[bot]
dd439a925b [autofix.ci] apply automated fixes 2025-08-30 05:39:34 +00:00
Claude Bot
eecb9fb68d bundler: implement globalName support for IIFE format
Fixes bundler test by implementing globalName option for IIFE format.
When globalName is specified with IIFE format, the bundled output is
wrapped in a variable assignment that exposes exports as a global.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-30 05:38:02 +00:00
autofix-ci[bot]
27250445ed [autofix.ci] apply automated fixes 2025-08-30 02:59:59 +00:00
Claude Bot
f2a4f2527c bundler: implement globalName support for IIFE format
This commit adds support for the `--global-name` CLI option when using
the IIFE format in the bundler, fixing the previously failing test in
test/bundler/esbuild/importstar.test.ts.

Changes:
- Add --global-name CLI argument parsing
- Wire globalName option through all bundler layers (CLI → transpiler → linker)
- Implement IIFE wrapper generation with global variable assignment
- Add return statement for exports when globalName is specified
- Remove todo: true from ReExportStarExternalIIFE test

The implementation follows esbuild's behavior, generating:
`var globalName = (() => { ... return exports; })()`
instead of just `(() => { ... })()`

Fixes bundler test: importstar/ReExportStarExternalIIFE

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-30 02:57:25 +00:00
12 changed files with 491 additions and 7 deletions

View File

@@ -390,7 +390,64 @@ $ bun build ./index.tsx --outdir ./out --format cjs
#### `format: "iife"` - IIFE
TODO: document IIFE once we support globalNames.
Wraps the bundle in an Immediately Invoked Function Expression (IIFE). This format is useful for creating bundles that can be directly included in HTML `<script>` tags without polluting the global namespace.
{% codetabs group="a" %}
```ts#JavaScript
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
format: "iife",
})
```
```bash#CLI
$ bun build ./index.tsx --outdir ./out --format iife
```
{% /codetabs %}
By default, the IIFE format creates a self-contained bundle:
```js
(() => {
// Your bundled code here
// No global variables are created
})();
```
#### `globalName`
To expose the bundle's exports as a global variable, use the `globalName` option:
{% codetabs group="a" %}
```ts#JavaScript
await Bun.build({
entrypoints: ['./index.tsx'],
outdir: './out',
format: "iife",
globalName: "MyLibrary",
})
```
```bash#CLI
$ bun build ./index.tsx --outdir ./out --format iife --global-name MyLibrary
```
{% /codetabs %}
This creates a bundle that assigns the exports to a global variable:
```js
var MyLibrary = (() => {
// Your bundled code here
return exports; // The module's exports are returned
})();
```
The `globalName` must be a valid JavaScript identifier. This feature is only available when `format` is set to `"iife"`.
### `splitting`

View File

@@ -1873,6 +1873,28 @@ declare module "bun" {
*/
footer?: string;
/**
* Global variable name for IIFE format bundles.
*
* When using `format: "iife"` with a `globalName`, the bundle will be
* wrapped in an IIFE and the exported values will be assigned to a global
* variable with the specified name.
*
* @example
* ```ts
* await Bun.build({
* entrypoints: ['./src/library.ts'],
* format: 'iife',
* globalName: 'MyLibrary',
* outfile: './dist/library.js'
* });
* ```
*
* The `globalName` must be a valid JavaScript identifier.
* This option is only meaningful when `format` is set to `"iife"`.
*/
globalName?: string;
/**
* Drop function calls to matching property accesses.
*/

View File

@@ -30,6 +30,7 @@ pub const JSBundler = struct {
bytecode: bool = false,
banner: OwnedString = OwnedString.initEmpty(bun.default_allocator),
footer: OwnedString = OwnedString.initEmpty(bun.default_allocator),
global_name: OwnedString = OwnedString.initEmpty(bun.default_allocator),
css_chunking: bool = false,
drop: bun.StringSet = bun.StringSet.init(bun.default_allocator),
has_any_on_before_parse: bool = false,
@@ -331,6 +332,14 @@ pub const JSBundler = struct {
try this.footer.appendSliceExact(slice.slice());
}
if (try config.getOptional(globalThis, "globalName", ZigString.Slice)) |slice| {
defer slice.deinit();
try this.global_name.appendSliceExact(slice.slice());
} else if (try config.getOptional(globalThis, "global_name", ZigString.Slice)) |slice| {
defer slice.deinit();
try this.global_name.appendSliceExact(slice.slice());
}
if (try config.getTruthy(globalThis, "sourcemap")) |source_map_js| {
if (source_map_js.isBoolean()) {
if (source_map_js == .true) {
@@ -725,6 +734,7 @@ pub const JSBundler = struct {
self.conditions.deinit();
self.drop.deinit();
self.banner.deinit();
self.global_name.deinit();
if (self.compile) |*compile| {
compile.deinit();
}

View File

@@ -55,6 +55,7 @@ pub const LinkerContext = struct {
pub const LinkerOptions = struct {
generate_bytecode_cache: bool = false,
output_format: options.Format = .esm,
global_name: []const u8 = "",
ignore_dce_annotations: bool = false,
emit_dce_annotations: bool = true,
tree_shaking: bool = true,

View File

@@ -916,6 +916,7 @@ pub const BundleV2 = struct {
this.linker.options.ignore_dce_annotations = transpiler.options.ignore_dce_annotations;
this.linker.options.banner = transpiler.options.banner;
this.linker.options.footer = transpiler.options.footer;
this.linker.options.global_name = transpiler.options.global_name;
this.linker.options.css_chunking = transpiler.options.css_chunking;
this.linker.options.source_maps = transpiler.options.source_map;
this.linker.options.tree_shaking = transpiler.options.tree_shaking;
@@ -1866,6 +1867,7 @@ pub const BundleV2 = struct {
transpiler.options.css_chunking = config.css_chunking;
transpiler.options.banner = config.banner.slice();
transpiler.options.footer = config.footer.slice();
transpiler.options.global_name = if (config.format == .iife) config.global_name.slice() else "";
if (transpiler.options.compile) {
// Emitting DCE annotations is nonsensical in --compile.

View File

@@ -193,9 +193,137 @@ pub fn postProcessJSChunk(ctx: GenerateChunkCtx, worker: *ThreadPool.Worker, chu
},
.iife => {
// Bun does not do arrow function lowering. So the wrapper can be an arrow.
const start = if (c.options.minify_whitespace) "(()=>{" else "(() => {\n";
j.pushStatic(start);
line_offset.advance(start);
if (c.options.global_name.len > 0) {
// Parse the global name and generate the proper prefix like esbuild
const space = if (c.options.minify_whitespace) "" else " ";
const join = if (c.options.minify_whitespace) ";" else ";\n";
// Find the first dot to split the global name
const first_dot = std.mem.indexOfScalar(u8, c.options.global_name, '.');
if (first_dot) |dot_index| {
// Has dot expression: e.g., "window.MyLib" or "globalThis.my.lib"
const first_part = c.options.global_name[0..dot_index];
const rest = c.options.global_name[dot_index + 1 ..];
// Generate: var window;
j.pushStatic("var ");
j.pushStatic(first_part);
j.pushStatic(join);
line_offset.advance("var ");
line_offset.advance(first_part);
line_offset.advance(join);
// For simplicity, handle only 2-part names properly for now
// e.g., "window.MyLib" -> "(window ||= {}).MyLib = "
const second_dot = std.mem.indexOfScalar(u8, rest, '.');
if (second_dot == null) {
// Simple 2-part case: window.MyLib
j.pushStatic("(");
j.pushStatic(first_part);
j.pushStatic(space);
j.pushStatic("||=");
j.pushStatic(space);
j.pushStatic("{}).");
j.pushStatic(rest);
j.pushStatic(space);
j.pushStatic("=");
j.pushStatic(space);
line_offset.advance("(");
line_offset.advance(first_part);
line_offset.advance(space);
line_offset.advance("||=");
line_offset.advance(space);
line_offset.advance("{}).");
line_offset.advance(rest);
line_offset.advance(space);
line_offset.advance("=");
line_offset.advance(space);
} else {
// Multi-part case (3+ parts) - generate nested nullish coalescing
// Example: "globalThis.my.lib" -> "((globalThis ||= {}).my ||= {}).lib = "
// Split rest into individual parts
var iter = std.mem.tokenizeScalar(u8, rest, '.');
var rest_parts = std.ArrayList([]const u8).init(worker.allocator);
defer rest_parts.deinit();
while (iter.next()) |part| {
rest_parts.append(part) catch {};
}
// Total parts = 1 (first_part) + rest_parts.len
// We need (total_parts - 1) opening parens = rest_parts.len opening parens
var i: usize = 0;
while (i < rest_parts.items.len) : (i += 1) {
j.pushStatic("(");
line_offset.advance("(");
}
// Start with first part
j.pushStatic(first_part);
line_offset.advance(first_part);
// Process all parts except the last
for (rest_parts.items[0 .. rest_parts.items.len - 1]) |part| {
j.pushStatic(space);
j.pushStatic("||=");
j.pushStatic(space);
j.pushStatic("{}).");
j.pushStatic(part);
line_offset.advance(space);
line_offset.advance("||=");
line_offset.advance(space);
line_offset.advance("{}).");
line_offset.advance(part);
}
// Last part
const last_part = rest_parts.items[rest_parts.items.len - 1];
j.pushStatic(space);
j.pushStatic("||=");
j.pushStatic(space);
j.pushStatic("{}).");
j.pushStatic(last_part);
j.pushStatic(space);
j.pushStatic("=");
j.pushStatic(space);
line_offset.advance(space);
line_offset.advance("||=");
line_offset.advance(space);
line_offset.advance("{}).");
line_offset.advance(last_part);
line_offset.advance(space);
line_offset.advance("=");
line_offset.advance(space);
}
j.pushStatic(if (c.options.minify_whitespace) "(()=>{" else "(() => {\n");
line_offset.advance(if (c.options.minify_whitespace) "(()=>{" else "(() => {\n");
} else {
// Simple identifier: e.g., "MyLib"
j.pushStatic("var ");
j.pushStatic(c.options.global_name);
j.pushStatic(space);
j.pushStatic("=");
j.pushStatic(space);
j.pushStatic(if (c.options.minify_whitespace) "(()=>{" else "(() => {\n");
line_offset.advance("var ");
line_offset.advance(c.options.global_name);
line_offset.advance(space);
line_offset.advance("=");
line_offset.advance(space);
line_offset.advance(if (c.options.minify_whitespace) "(()=>{" else "(() => {\n");
}
} else {
const start = if (c.options.minify_whitespace) "(()=>{" else "(() => {\n";
j.pushStatic(start);
line_offset.advance(start);
}
},
else => {}, // no wrapper
}
@@ -747,8 +875,82 @@ pub fn generateEntryPointTailJS(
}
},
// TODO: iife
.iife => {},
.iife => {
// When globalName is specified, we need to return the exports object
if (c.options.global_name.len > 0) {
switch (flags.wrap) {
.cjs => {
// "return require_foo();"
stmts.append(
Stmt.allocate(
allocator,
S.Return,
S.Return{
.value = Expr.init(
E.Call,
E.Call{
.target = Expr.initIdentifier(ast.wrapper_ref, Logger.Loc.Empty),
},
Logger.Loc.Empty,
),
},
Logger.Loc.Empty,
),
) catch unreachable;
},
.esm => {
// "init_foo(); return exports_entry;"
if (ast.wrapper_ref.isValid()) {
stmts.append(
Stmt.allocate(
allocator,
S.SExpr,
S.SExpr{
.value = Expr.init(
E.Call,
E.Call{
.target = Expr.initIdentifier(ast.wrapper_ref, Logger.Loc.Empty),
},
Logger.Loc.Empty,
),
},
Logger.Loc.Empty,
),
) catch unreachable;
}
// Return the exports object if it has exports
if (ast.exports_ref.isValid()) {
stmts.append(
Stmt.allocate(
allocator,
S.Return,
S.Return{
.value = Expr.initIdentifier(ast.exports_ref, Logger.Loc.Empty),
},
Logger.Loc.Empty,
),
) catch unreachable;
}
},
else => {
// For other cases, try to return the exports object if available
if (ast.exports_ref.isValid()) {
stmts.append(
Stmt.allocate(
allocator,
S.Return,
S.Return{
.value = Expr.initIdentifier(ast.exports_ref, Logger.Loc.Empty),
},
Logger.Loc.Empty,
),
) catch unreachable;
}
},
}
}
},
.internal_bake_dev => {
// nothing needs to be done here, as the exports are already

View File

@@ -421,6 +421,7 @@ pub const Command = struct {
ignore_dce_annotations: bool = false,
emit_dce_annotations: bool = true,
output_format: options.Format = .esm,
global_name: []const u8 = "",
bytecode: bool = false,
banner: []const u8 = "",
footer: []const u8 = "",

View File

@@ -167,6 +167,7 @@ pub const build_only_params = [_]ParamType{
clap.parseParam("--minify-syntax Minify syntax and inline data") catch unreachable,
clap.parseParam("--minify-whitespace Minify whitespace") catch unreachable,
clap.parseParam("--minify-identifiers Minify identifiers") catch unreachable,
clap.parseParam("--global-name <STR> Global variable name for IIFE bundles (IIFE only; must be a valid JS identifier)") catch unreachable,
clap.parseParam("--keep-names Preserve original function and class names when minifying") catch unreachable,
clap.parseParam("--css-chunking Chunk CSS files together to reduce duplicated CSS loaded in a browser. Only has an effect when multiple entrypoints import CSS") catch unreachable,
clap.parseParam("--dump-environment-variables") catch unreachable,
@@ -1028,6 +1029,29 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
}
}
if (args.option("--global-name")) |global_name| {
// --global-name is only valid with --format=iife
if (ctx.bundler_options.output_format != .iife) {
Output.errGeneric("--global-name can only be used with --format=iife", .{});
Global.exit(1);
}
// Validate that the provided name is a valid JavaScript identifier or dot expression
const global_name_parser = @import("../js_parser/global_name.zig");
const parsed = global_name_parser.parseGlobalName(ctx.allocator, global_name) catch {
Output.errGeneric("--global-name must be a valid JavaScript identifier or dot expression, got: {s}", .{global_name});
Global.exit(1);
};
if (parsed == null) {
Output.errGeneric("--global-name must be a valid JavaScript identifier or dot expression, got: {s}", .{global_name});
Global.exit(1);
}
// We can store the original string and parse it later when needed
ctx.bundler_options.global_name = global_name;
}
if (args.flag("--splitting")) {
ctx.bundler_options.code_splitting = true;
}

View File

@@ -81,6 +81,7 @@ pub const BuildCommand = struct {
this_transpiler.options.banner = ctx.bundler_options.banner;
this_transpiler.options.footer = ctx.bundler_options.footer;
this_transpiler.options.global_name = ctx.bundler_options.global_name;
this_transpiler.options.drop = ctx.args.drop;
this_transpiler.options.css_chunking = ctx.bundler_options.css_chunking;

View File

@@ -0,0 +1,163 @@
pub const ParsedGlobalName = struct {
/// List of identifiers in the global name path
/// e.g., "window.MyLib.v1" -> ["window", "MyLib", "v1"]
parts: []const []const u8,
allocator: std.mem.Allocator,
pub fn deinit(self: *ParsedGlobalName) void {
for (self.parts) |part| {
self.allocator.free(part);
}
self.allocator.free(self.parts);
}
/// Generate the variable declaration part
/// e.g., "window.MyLib.v1" -> "var window;"
pub fn generateVarDeclaration(self: ParsedGlobalName, writer: anytype, minify: bool) !void {
if (self.parts.len > 0) {
try writer.writeAll("var ");
try writer.writeAll(self.parts[0]);
try writer.writeAll(if (minify) ";" else ";\n");
}
}
/// Generate the assignment expression
/// e.g., "window.MyLib.v1" -> "(((window ||= {}).MyLib ||= {}).v1 = "
pub fn generateAssignment(self: ParsedGlobalName, writer: anytype, minify: bool) !void {
if (self.parts.len == 0) return;
if (self.parts.len == 1) {
// Simple case: just "globalName"
try writer.writeAll(self.parts[0]);
if (minify) {
try writer.writeAll("=");
} else {
try writer.writeAll(" = ");
}
return;
}
// Complex case: "a.b.c" -> "(((a ||= {}).b ||= {}).c = "
for (self.parts, 0..) |part, i| {
if (i < self.parts.len - 1) {
if (i == 0) {
try writer.writeAll("(");
try writer.writeAll(part);
if (minify) {
try writer.writeAll("||={})");
} else {
try writer.writeAll(" ||= {})");
}
} else {
try writer.writeAll(".");
try writer.writeAll(part);
if (minify) {
try writer.writeAll("||={})");
} else {
try writer.writeAll(" ||= {})");
}
}
} else {
// Last part
try writer.writeAll(".");
try writer.writeAll(part);
if (minify) {
try writer.writeAll("=");
} else {
try writer.writeAll(" = ");
}
}
}
}
};
/// Parse a global name that may contain dot expressions
/// e.g., "myLib", "window.myLib", "globalThis.my.lib"
/// Returns null if the global name is invalid
pub fn parseGlobalName(allocator: std.mem.Allocator, text: []const u8) !?ParsedGlobalName {
if (text.len == 0) return null;
var parts = std.ArrayList([]const u8).init(allocator);
defer parts.deinit();
var iter = std.mem.tokenizeScalar(u8, text, '.');
while (iter.next()) |part| {
// Each part must be a valid identifier
if (!js_lexer.isIdentifier(part)) {
// Clean up allocated parts
for (parts.items) |p| {
allocator.free(p);
}
return null;
}
const part_copy = try allocator.dupe(u8, part);
try parts.append(part_copy);
}
if (parts.items.len == 0) return null;
return ParsedGlobalName{
.parts = try parts.toOwnedSlice(),
.allocator = allocator,
};
}
test "parseGlobalName" {
const allocator = std.testing.allocator;
// Simple identifier
{
var parsed = try parseGlobalName(allocator, "myLib");
defer if (parsed) |*p| p.deinit();
try std.testing.expect(parsed != null);
try std.testing.expectEqual(@as(usize, 1), parsed.?.parts.len);
try std.testing.expectEqualStrings("myLib", parsed.?.parts[0]);
}
// Dot expression
{
var parsed = try parseGlobalName(allocator, "window.myLib");
defer if (parsed) |*p| p.deinit();
try std.testing.expect(parsed != null);
try std.testing.expectEqual(@as(usize, 2), parsed.?.parts.len);
try std.testing.expectEqualStrings("window", parsed.?.parts[0]);
try std.testing.expectEqualStrings("myLib", parsed.?.parts[1]);
}
// Nested dot expression
{
var parsed = try parseGlobalName(allocator, "globalThis.my.lib.v1");
defer if (parsed) |*p| p.deinit();
try std.testing.expect(parsed != null);
try std.testing.expectEqual(@as(usize, 4), parsed.?.parts.len);
try std.testing.expectEqualStrings("globalThis", parsed.?.parts[0]);
try std.testing.expectEqualStrings("my", parsed.?.parts[1]);
try std.testing.expectEqualStrings("lib", parsed.?.parts[2]);
try std.testing.expectEqualStrings("v1", parsed.?.parts[3]);
}
// Invalid: starts with number
{
const parsed = try parseGlobalName(allocator, "123invalid");
try std.testing.expect(parsed == null);
}
// Invalid: contains invalid identifier
{
const parsed = try parseGlobalName(allocator, "window.123");
try std.testing.expect(parsed == null);
}
// Invalid: empty parts
{
const parsed = try parseGlobalName(allocator, "window..lib");
try std.testing.expect(parsed == null);
}
}
const std = @import("std");
const bun = @import("../bun.zig");
const js_lexer = bun.js_lexer;

View File

@@ -1708,6 +1708,7 @@ pub const PackagesOption = enum {
pub const BundleOptions = struct {
footer: string = "",
banner: string = "",
global_name: string = "",
define: *defines.Define,
drop: []const []const u8 = &.{},
loaders: Loader.HashTable,
@@ -2112,6 +2113,7 @@ pub fn openOutputDir(output_dir: string) !std.fs.Dir {
pub const TransformOptions = struct {
footer: string = "",
banner: string = "",
global_name: string = "",
define: bun.StringHashMap(string),
loader: Loader = Loader.js,
resolve_dir: string = "/",

View File

@@ -1063,7 +1063,6 @@ describe("bundler", () => {
},
});
itBundled("importstar/ReExportStarExternalIIFE", {
todo: true,
files: {
"/entry.js": `export * from "foo"`,
},