diff --git a/docs/bundler/index.md b/docs/bundler/index.md index 875729eaedad6..0a5d40b6221a6 100644 --- a/docs/bundler/index.md +++ b/docs/bundler/index.md @@ -1092,7 +1092,7 @@ $ bun build ./index.tsx --outdir ./out --loader .png:dataurl --loader .txt:file ### `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. +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 %} @@ -1108,11 +1108,29 @@ await Bun.build({ $ bun build ./index.tsx --outdir ./out --banner "\"use client\";" ``` +### `footer` + +A footer to be added to the final bundle, this can be something like a comment block for a license or just a fun easter egg. + +{% codetabs %} + +```ts#JavaScript +await Bun.build({ + entrypoints: ['./index.tsx'], + outdir: './out', + footer: '// built with love in SF' +}) +``` + +```bash#CLI +$ bun build ./index.tsx --outdir ./out --footer="// built with love in SF" +``` + {% /codetabs %} ### `experimentalCss` -Whether to enable *experimental* support for bundling CSS files. Defaults to `false`. +Whether to enable _experimental_ support for bundling CSS files. Defaults to `false`. This supports bundling CSS files imported from JS, as well as CSS entrypoints. diff --git a/docs/bundler/vs-esbuild.md b/docs/bundler/vs-esbuild.md index fe6a96e542243..8a42354da5eac 100644 --- a/docs/bundler/vs-esbuild.md +++ b/docs/bundler/vs-esbuild.md @@ -159,6 +159,12 @@ In Bun's CLI, simple boolean flags like `--minify` do not accept an argument. Ot --- +- `--footer` +- `--footer` +- Only applies to js bundles + +--- + - `--certfile` - n/a - Not applicable @@ -195,12 +201,6 @@ In Bun's CLI, simple boolean flags like `--minify` do not accept an argument. Ot --- -- `--footer` -- n/a -- Not supported - ---- - - `--global-name` - n/a - Not applicable, Bun does not support `iife` output at this time diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 12c54090b69b6..6e7ed1cdb8f29 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -1599,6 +1599,12 @@ declare module "bun" { * Add a banner to the bundled code such as "use client"; */ banner?: string; + /** + * Add a footer to the bundled code such as a comment block like + * + * `// made with bun!` + */ + footer?: string; /** * **Experimental** diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index 557e45ee0c8be..82b517c499e50 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -73,6 +73,7 @@ pub const JSBundler = struct { format: options.Format = .esm, bytecode: bool = false, banner: OwnedString = OwnedString.initEmpty(bun.default_allocator), + footer: OwnedString = OwnedString.initEmpty(bun.default_allocator), experimental_css: bool = false, pub const List = bun.StringArrayHashMapUnmanaged(Config); @@ -190,6 +191,12 @@ pub const JSBundler = struct { try this.banner.appendSliceExact(slice.slice()); } + + if (try config.getOptional(globalThis, "footer", ZigString.Slice)) |slice| { + defer slice.deinit(); + try this.footer.appendSliceExact(slice.slice()); + } + if (config.getTruthy(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 e169ecdba2c82..3cc46fbe3ab37 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -876,6 +876,7 @@ pub const BundleV2 = struct { this.linker.options.ignore_dce_annotations = bundler.options.ignore_dce_annotations; this.linker.options.banner = bundler.options.banner; + this.linker.options.footer = bundler.options.footer; this.linker.options.experimental_css = bundler.options.experimental_css; @@ -1478,6 +1479,7 @@ pub const BundleV2 = struct { bundler.options.ignore_dce_annotations = config.ignore_dce_annotations; bundler.options.experimental_css = config.experimental_css; bundler.options.banner = config.banner.toOwnedSlice(); + bundler.options.footer = config.footer.toOwnedSlice(); bundler.configureLinker(); try bundler.configureDefines(); @@ -4602,6 +4604,7 @@ pub const LinkerContext = struct { minify_syntax: bool = false, minify_identifiers: bool = false, banner: []const u8 = "", + footer: []const u8 = "", experimental_css: bool = false, source_maps: options.SourceMapOption = .none, target: options.Target = .browser, @@ -8977,7 +8980,16 @@ pub const LinkerContext = struct { j.ensureNewlineAtEnd(); // TODO: maybeAppendLegalComments - // TODO: footer + if (c.options.footer.len > 0) { + if (newline_before_comment) { + j.pushStatic("\n"); + line_offset.advance("\n"); + } + j.pushStatic(ctx.c.options.footer); + line_offset.advance(ctx.c.options.footer); + j.pushStatic("\n"); + line_offset.advance("\n"); + } chunk.intermediate_output = c.breakOutputIntoPieces( worker.allocator, diff --git a/src/cli.zig b/src/cli.zig index 34c94a7b500c4..61141fe939290 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -263,6 +263,7 @@ pub const Arguments = struct { 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("--footer Add a footer to the bundled output such as // built with bun!") 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, @@ -783,6 +784,10 @@ pub const Arguments = struct { ctx.bundler_options.banner = banner; } + if (args.option("--footer")) |footer| { + ctx.bundler_options.footer = footer; + } + const experimental_css = args.flag("--experimental-css"); ctx.bundler_options.experimental_css = experimental_css; @@ -1408,6 +1413,7 @@ pub const Command = struct { output_format: options.Format = .esm, bytecode: bool = false, banner: []const u8 = "", + footer: []const u8 = "", experimental_css: bool = false, }; diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig index 4c4651b4924e5..d973ef5b75e50 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -98,6 +98,8 @@ pub const BuildCommand = struct { this_bundler.options.ignore_dce_annotations = ctx.bundler_options.ignore_dce_annotations; this_bundler.options.banner = ctx.bundler_options.banner; + this_bundler.options.footer = ctx.bundler_options.footer; + 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_footer.test.ts b/test/bundler/bundler_footer.test.ts new file mode 100644 index 0000000000000..3f79d46fe5dac --- /dev/null +++ b/test/bundler/bundler_footer.test.ts @@ -0,0 +1,29 @@ +import { describe } from "bun:test"; +import { itBundled } from "./expectBundled"; + +describe("bundler", () => { + itBundled("footer/CommentFooter", { + footer: "// developed with love in SF", + files: { + "/a.js": `console.log("Hello, world!")`, + }, + onAfterBundle(api) { + api.expectFile("out.js").toEndWith('// developed with love in SF"\n'); + }, + }); + itBundled("footer/MultilineFooter", { + footer: `/** + * This is copyright of [...] ${new Date().getFullYear()} + * do not redistribute without consent of [...] +*/`, + files: { + "index.js": `console.log("Hello, world!")`, + }, + onAfterBundle(api) { + api.expectFile("out.js").toEndWith(`/** + * This is copyright of [...] ${new Date().getFullYear()} + * do not redistribute without consent of [...] +*/\"\n`); + }, + }); +}); diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index 8be08a71a3af3..e1dca715319d1 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -146,6 +146,7 @@ export interface BundlerTestInput { alias?: Record; assetNaming?: string; banner?: string; + footer?: string; define?: Record; /** Use for resolve custom conditions */ @@ -416,6 +417,7 @@ function expectBundled( external, packages, files, + footer, format, globalName, inject, @@ -666,6 +668,7 @@ function expectBundled( serverComponents && "--server-components", outbase && `--root=${outbase}`, banner && `--banner="${banner}"`, // TODO: --banner-css=* + footer && `--footer="${footer}"`, ignoreDCEAnnotations && `--ignore-dce-annotations`, emitDCEAnnotations && `--emit-dce-annotations`, // inject && inject.map(x => ["--inject", path.join(root, x)]), @@ -710,6 +713,7 @@ function expectBundled( metafile && `--metafile=${metafile}`, sourceMap && `--sourcemap=${sourceMap}`, banner && `--banner:js=${banner}`, + footer && `--footer:js=${footer}`, legalComments && `--legal-comments=${legalComments}`, ignoreDCEAnnotations && `--ignore-annotations`, splitting && `--splitting`,