diff --git a/docs/bundler/index.md b/docs/bundler/index.md index d5598ec2c6ea0..875729eaedad6 100644 --- a/docs/bundler/index.md +++ b/docs/bundler/index.md @@ -1090,6 +1090,26 @@ $ bun build ./index.tsx --outdir ./out --loader .png:dataurl --loader .txt:file {% /codetabs %} +### `banner` + +A banner to be added to the final bundle, this can be a directive like "use client" for react or a comment block such as a license for the code. + +{% codetabs %} + +```ts#JavaScript +await Bun.build({ + entrypoints: ['./index.tsx'], + outdir: './out', + banner: '"use client";' +}) +``` + +```bash#CLI +$ bun build ./index.tsx --outdir ./out --banner "\"use client\";" +``` + +{% /codetabs %} + ### `experimentalCss` Whether to enable *experimental* support for bundling CSS files. Defaults to `false`. diff --git a/docs/bundler/vs-esbuild.md b/docs/bundler/vs-esbuild.md index a3acb93c9afa7..fe6a96e542243 100644 --- a/docs/bundler/vs-esbuild.md +++ b/docs/bundler/vs-esbuild.md @@ -154,8 +154,8 @@ In Bun's CLI, simple boolean flags like `--minify` do not accept an argument. Ot --- - `--banner` -- n/a -- Not supported +- `--banner` +- Only applies to js bundles --- diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index ebd1019aa82dd..12c54090b69b6 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -1595,6 +1595,10 @@ declare module "bun" { * @default false */ bytecode?: boolean; + /** + * Add a banner to the bundled code such as "use client"; + */ + banner?: string; /** * **Experimental** diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index afe5f7bef0cd7..08af5dae854d0 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -72,6 +72,7 @@ pub const JSBundler = struct { packages: options.PackagesOption = .bundle, format: options.Format = .esm, bytecode: bool = false, + banner: OwnedString = OwnedString.initEmpty(bun.default_allocator), experimental_css: bool = false, pub const List = bun.StringArrayHashMapUnmanaged(Config); @@ -184,6 +185,11 @@ pub const JSBundler = struct { has_out_dir = true; } + if (try config.getOwnOptional(globalThis, "banner", ZigString.Slice)) |slice| { + defer slice.deinit(); + try this.banner.appendSliceExact(slice.slice()); + } + if (config.getOwnTruthy(globalThis, "sourcemap")) |source_map_js| { if (bun.FeatureFlags.breaking_changes_1_2 and config.isBoolean()) { if (source_map_js == .true) { diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 01995087092c9..e169ecdba2c82 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -875,6 +875,8 @@ pub const BundleV2 = struct { this.linker.options.emit_dce_annotations = bundler.options.emit_dce_annotations; this.linker.options.ignore_dce_annotations = bundler.options.ignore_dce_annotations; + this.linker.options.banner = bundler.options.banner; + this.linker.options.experimental_css = bundler.options.experimental_css; this.linker.options.source_maps = bundler.options.source_map; @@ -1475,6 +1477,7 @@ pub const BundleV2 = struct { bundler.options.emit_dce_annotations = config.emit_dce_annotations orelse !config.minify.whitespace; bundler.options.ignore_dce_annotations = config.ignore_dce_annotations; bundler.options.experimental_css = config.experimental_css; + bundler.options.banner = config.banner.toOwnedSlice(); bundler.configureLinker(); try bundler.configureDefines(); @@ -4598,6 +4601,7 @@ pub const LinkerContext = struct { minify_whitespace: bool = false, minify_syntax: bool = false, minify_identifiers: bool = false, + banner: []const u8 = "", experimental_css: bool = false, source_maps: options.SourceMapOption = .none, target: options.Target = .browser, @@ -8751,7 +8755,16 @@ pub const LinkerContext = struct { } } - // TODO: banner + if (c.options.banner.len > 0) { + if (newline_before_comment) { + j.pushStatic("\n"); + line_offset.advance("\n"); + } + j.pushStatic(ctx.c.options.banner); + line_offset.advance(ctx.c.options.banner); + j.pushStatic("\n"); + line_offset.advance("\n"); + } // Add the top-level directive if present (but omit "use strict" in ES // modules because all ES modules are automatically in strict mode) diff --git a/src/cli.zig b/src/cli.zig index c3cb24ca0dc4f..34c94a7b500c4 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -262,6 +262,7 @@ pub const Arguments = struct { clap.parseParam("--outdir Default to \"dist\" if multiple files") catch unreachable, clap.parseParam("--outfile Write to a file") catch unreachable, clap.parseParam("--sourcemap ? Build with sourcemaps - 'linked', 'inline', 'external', or 'none'") catch unreachable, + clap.parseParam("--banner Add a banner to the bundled output such as \"use client\"; for a bundle being used with RSCs") catch unreachable, clap.parseParam("--format Specifies the module format to build to. Only \"esm\" is supported.") catch unreachable, clap.parseParam("--root Root directory used for multiple entry points") catch unreachable, clap.parseParam("--splitting Enable code splitting") catch unreachable, @@ -778,6 +779,10 @@ pub const Arguments = struct { ctx.bundler_options.public_path = public_path; } + if (args.option("--banner")) |banner| { + ctx.bundler_options.banner = banner; + } + const experimental_css = args.flag("--experimental-css"); ctx.bundler_options.experimental_css = experimental_css; @@ -1402,6 +1407,7 @@ pub const Command = struct { emit_dce_annotations: bool = true, output_format: options.Format = .esm, bytecode: bool = false, + banner: []const u8 = "", experimental_css: bool = false, }; diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig index 754e550e36f9f..4c4651b4924e5 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -97,6 +97,7 @@ pub const BuildCommand = struct { this_bundler.options.emit_dce_annotations = ctx.bundler_options.emit_dce_annotations; this_bundler.options.ignore_dce_annotations = ctx.bundler_options.ignore_dce_annotations; + this_bundler.options.banner = ctx.bundler_options.banner; this_bundler.options.experimental_css = ctx.bundler_options.experimental_css; this_bundler.options.output_dir = ctx.bundler_options.outdir; diff --git a/test/bundler/bundler_banner.test.ts b/test/bundler/bundler_banner.test.ts new file mode 100644 index 0000000000000..8a783b66fc175 --- /dev/null +++ b/test/bundler/bundler_banner.test.ts @@ -0,0 +1,36 @@ +import { describe } from "bun:test"; +import { itBundled } from "./expectBundled"; + +describe("bundler", () => { + itBundled("banner/CommentBanner", { + banner: "// developed with love in SF", + files: { + "/a.js": `console.log("Hello, world!")`, + }, + onAfterBundle(api) { + api.expectFile("out.js").toContain("// developed with love in SF"); + }, + }); + itBundled("banner/MultilineBanner", { + banner: `"use client"; +// This is a multiline banner +// It can contain multiple lines of comments or code`, + files: { + /* js*/ "index.js": `console.log("Hello, world!")`, + }, + onAfterBundle(api) { + api.expectFile("out.js").toContain(`"use client"; +// This is a multiline banner +// It can contain multiple lines of comments or code`); + }, + }); + itBundled("banner/UseClientBanner", { + banner: '"use client";', + files: { + /* js*/ "index.js": `console.log("Hello, world!")`, + }, + onAfterBundle(api) { + api.expectFile("out.js").toContain('"use client";'); + }, + }); +}); diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index 43240263f9b19..8be08a71a3af3 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -120,7 +120,6 @@ export interface BundlerTestInput { /** Temporary flag to mark failing tests as skipped. */ todo?: boolean; - // file options files: Record; /** Files to be written only after the bundle is done. */ @@ -515,9 +514,6 @@ function expectBundled( if (!ESBUILD && mainFields) { throw new Error("mainFields not implemented in bun build"); } - if (!ESBUILD && banner) { - throw new Error("banner not implemented in bun build"); - } if (!ESBUILD && inject) { throw new Error("inject not implemented in bun build"); } @@ -669,6 +665,7 @@ function expectBundled( splitting && `--splitting`, serverComponents && "--server-components", outbase && `--root=${outbase}`, + banner && `--banner="${banner}"`, // TODO: --banner-css=* ignoreDCEAnnotations && `--ignore-dce-annotations`, emitDCEAnnotations && `--emit-dce-annotations`, // inject && inject.map(x => ["--inject", path.join(root, x)]), @@ -1532,7 +1529,7 @@ for (const [key, blob] of build.outputs) { let result = out!.toUnixString().trim(); // no idea why this logs. ¯\_(ツ)_/¯ - result = result.replace(`[EventLoop] enqueueTaskConcurrent(RuntimeTranspilerStore)\n`, ''); + result = result.replace(`[EventLoop] enqueueTaskConcurrent(RuntimeTranspilerStore)\n`, ""); if (typeof expected === "string") { expected = dedent(expected).trim(); @@ -1607,10 +1604,8 @@ export function itBundled( id, () => expectBundled(id, opts as any), // sourcemap code is slow - (opts.snapshotSourceMap - ? isDebug ? Infinity : 30_000 - : isDebug ? 15_000 : 5_000) - * ((isDebug ? opts.debugTimeoutScale : opts.timeoutScale) ?? 1), + (opts.snapshotSourceMap ? (isDebug ? Infinity : 30_000) : isDebug ? 15_000 : 5_000) * + ((isDebug ? opts.debugTimeoutScale : opts.timeoutScale) ?? 1), ); } return ref; @@ -1622,10 +1617,8 @@ itBundled.only = (id: string, opts: BundlerTestInput) => { id, () => expectBundled(id, opts as any), // sourcemap code is slow - (opts.snapshotSourceMap - ? isDebug ? Infinity : 30_000 - : isDebug ? 15_000 : 5_000) - * ((isDebug ? opts.debugTimeoutScale : opts.timeoutScale) ?? 1), + (opts.snapshotSourceMap ? (isDebug ? Infinity : 30_000) : isDebug ? 15_000 : 5_000) * + ((isDebug ? opts.debugTimeoutScale : opts.timeoutScale) ?? 1), ); };