Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/bun.js/api/JSBundler.zig
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub const JSBundler = struct {
banner: OwnedString = OwnedString.initEmpty(bun.default_allocator),
footer: OwnedString = OwnedString.initEmpty(bun.default_allocator),
css_chunking: bool = false,
legal_comments: options.LegalComments = .eof,
drop: bun.StringSet = bun.StringSet.init(bun.default_allocator),
has_any_on_before_parse: bool = false,
throw_on_error: bool = true,
Expand Down Expand Up @@ -162,6 +163,16 @@ pub const JSBundler = struct {
try this.footer.appendSliceExact(slice.slice());
}

if (try config.get(globalThis, "legalComments")) |legal_comments| {
if (!legal_comments.isUndefined()) {
this.legal_comments = try legal_comments.toEnum(
globalThis,
"legalComments",
options.LegalComments,
);
}
}

if (try config.getTruthy(globalThis, "sourcemap")) |source_map_js| {
if (source_map_js.isBoolean()) {
if (source_map_js == .true) {
Expand Down
1 change: 1 addition & 0 deletions src/bundler/bundle_v2.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1751,6 +1751,7 @@ pub const BundleV2 = struct {
transpiler.options.emit_dce_annotations = config.emit_dce_annotations orelse !config.minify.whitespace;
transpiler.options.ignore_dce_annotations = config.ignore_dce_annotations;
transpiler.options.css_chunking = config.css_chunking;
transpiler.options.legal_comments = config.legal_comments;
transpiler.options.banner = config.banner.slice();
transpiler.options.footer = config.footer.slice();

Expand Down
1 change: 1 addition & 0 deletions src/cli.zig
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ pub const Command = struct {
banner: []const u8 = "",
footer: []const u8 = "",
css_chunking: bool = false,
legal_comments: options.LegalComments = .eof,

bake: bool = false,
bake_debug_dump_server: bool = false,
Expand Down
10 changes: 10 additions & 0 deletions src/cli/Arguments.zig
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,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("--legal-comments <STR>? Where to place legal comments. \"none\", \"inline\", \"eof\", \"linked\", \"external\" (default: \"eof\" when bundling, \"inline\" otherwise)") 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,
clap.parseParam("--conditions <STR>... Pass custom conditions to resolve") catch unreachable,
Expand Down Expand Up @@ -789,6 +790,15 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C

ctx.bundler_options.css_chunking = args.flag("--css-chunking");

// Set legal comments - default to "eof" when bundling, "inline" otherwise
if (args.option("--legal-comments")) |legal_comments_str| {
ctx.bundler_options.legal_comments = options.LegalComments.fromString(legal_comments_str);
} else {
// esbuild's default: "eof" when bundling, "inline" otherwise
// For now, we'll default to "eof" since this is in a bundling context
ctx.bundler_options.legal_comments = .eof;
}

ctx.bundler_options.emit_dce_annotations = args.flag("--emit-dce-annotations") or
!ctx.bundler_options.minify_whitespace;

Expand Down
22 changes: 21 additions & 1 deletion src/js_lexer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1859,9 +1859,29 @@ fn NewLexer_(
}
}

fn isLegalComment(text: []const u8) bool {
// Already have legal annotation (/*! or //!)
if (text.len > 2 and text[2] == '!') {
return true;
}

// Check for JSDoc legal comment patterns like esbuild
if (text.len > 3) {
// Check for @license, @preserve, @copyright
if (std.mem.indexOf(u8, text, "@license") != null or
std.mem.indexOf(u8, text, "@preserve") != null or
std.mem.indexOf(u8, text, "@copyright") != null)
{
return true;
}
}

return false;
}

fn scanCommentText(noalias lexer: *LexerType, for_pragma: bool) void {
const text = lexer.source.contents[lexer.start..lexer.end];
const has_legal_annotation = text.len > 2 and text[2] == '!';
const has_legal_annotation = isLegalComment(text);
const is_multiline_comment = text.len > 1 and text[1] == '*';

if (lexer.track_comments)
Expand Down
27 changes: 27 additions & 0 deletions src/options.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,32 @@ pub const SourceMapOption = enum {
});
};

pub const LegalComments = enum {
none,
@"inline",
eof,
linked,
external,

pub fn fromString(str: ?[]const u8) LegalComments {
const s = str orelse return .eof; // Default to eof when bundling, inline otherwise (handled in Arguments.zig)
if (strings.eqlComptime(s, "none")) return .none;
if (strings.eqlComptime(s, "inline")) return .@"inline";
if (strings.eqlComptime(s, "eof")) return .eof;
if (strings.eqlComptime(s, "linked")) return .linked;
if (strings.eqlComptime(s, "external")) return .external;
return .eof; // Default fallback
}

pub const Map = bun.ComptimeStringMap(LegalComments, .{
.{ "none", .none },
.{ "inline", .@"inline" },
.{ "eof", .eof },
.{ "linked", .linked },
.{ "external", .external },
});
};

pub const PackagesOption = enum {
bundle,
external,
Expand Down Expand Up @@ -1705,6 +1731,7 @@ pub const BundleOptions = struct {
preserve_symlinks: bool = false,
preserve_extensions: bool = false,
production: bool = false,
legal_comments: LegalComments = .eof,

// only used by bundle_v2
output_format: Format = .esm,
Expand Down
86 changes: 86 additions & 0 deletions test/bundler/bun-build-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -956,4 +956,90 @@ export { greeting };`,
process.chdir(originalCwd);
}
});

test("legalComments option", async () => {
const dir = tempDirWithFiles("bun-build-api-legal-comments", {
"entry.js": `/*!
* Legal comment with ! - should be preserved
* Copyright 2024 Test Corp
*/

/**
* @license MIT
* This should be preserved as it contains @license
*/

/**
* @preserve
* This should be preserved as it contains @preserve
*/

/**
* This is a regular JSDoc comment - should be removed
*/

//! Legal line comment - should be preserved

// Regular line comment - should be removed

console.log("hello world");`,
});

// Test default behavior (should preserve legal comments)
const build1 = await Bun.build({
entrypoints: [join(dir, "entry.js")],
});

expect(build1.success).toBe(true);
expect(build1.outputs).toHaveLength(1);
const output1 = await build1.outputs[0].text();

// Should preserve legal comments
expect(output1).toContain("Legal comment with ! - should be preserved");
expect(output1).toContain("@license MIT");
expect(output1).toContain("@preserve");
expect(output1).toContain("//! Legal line comment - should be preserved");

// Should remove regular comments
expect(output1).not.toContain("This is a regular JSDoc comment - should be removed");
expect(output1).not.toContain("Regular line comment - should be removed");

// Test legalComments: "eof" (explicit)
const build2 = await Bun.build({
entrypoints: [join(dir, "entry.js")],
legalComments: "eof",
});

expect(build2.success).toBe(true);
expect(build2.outputs).toHaveLength(1);
const output2 = await build2.outputs[0].text();

// Should still preserve legal comments
expect(output2).toContain("Legal comment with ! - should be preserved");
expect(output2).toContain("@license MIT");
expect(output2).toContain("@preserve");

// Test legalComments: "inline"
const build3 = await Bun.build({
entrypoints: [join(dir, "entry.js")],
legalComments: "inline",
});

expect(build3.success).toBe(true);
expect(build3.outputs).toHaveLength(1);
const output3 = await build3.outputs[0].text();

// Should still preserve legal comments
expect(output3).toContain("Legal comment with ! - should be preserved");
expect(output3).toContain("@license MIT");
expect(output3).toContain("@preserve");

// Test invalid legalComments value (should throw)
await expect(async () => {
await Bun.build({
entrypoints: [join(dir, "entry.js")],
legalComments: "invalid" as any,
});
}).toThrow();
});
});
Loading