diff --git a/completions/bun.zsh b/completions/bun.zsh index 46485f39bab9f7..a19a44bec8f81d 100644 --- a/completions/bun.zsh +++ b/completions/bun.zsh @@ -425,6 +425,7 @@ _bun_run_completion() { '--external[Exclude module from transpilation (can use * wildcards). ex: -e react]:external' \ '-e[Exclude module from transpilation (can use * wildcards). ex: -e react]:external' \ '--loader[Parse files with .ext:loader, e.g. --loader .js:jsx. Valid loaders: js, jsx, ts, tsx, json, toml, text, file, wasm, napi]:loader' \ + '--packages[Exclude dependencies from bundle, e.g. --packages external. Valid options: bundle, external]:packages' \ '-l[Parse files with .ext:loader, e.g. --loader .js:jsx. Valid loaders: js, jsx, ts, tsx, json, toml, text, file, wasm, napi]:loader' \ '--origin[Rewrite import URLs to start with --origin. Default: ""]:origin' \ '-u[Rewrite import URLs to start with --origin. Default: ""]:origin' \ diff --git a/docs/bundler/index.md b/docs/bundler/index.md index 514be87a9e7efc..d5524f8a8aca81 100644 --- a/docs/bundler/index.md +++ b/docs/bundler/index.md @@ -756,6 +756,25 @@ $ bun build ./index.tsx --outdir ./out --external '*' {% /codetabs %} +### `packages` + +Control whatever package dependencies are included to bundle or not. Possible values: `bundle` (default), `external`. Bun threats any import which path do not start with `.`, `..` or `/` as package. + +{% codetabs group="a" %} + +```ts#JavaScript +await Bun.build({ + entrypoints: ['./index.ts'], + packages: 'external', +}) +``` + +```bash#CLI +$ bun build ./index.ts --packages external +``` + +{% /codetabs %} + ### `naming` Customizes the generated file names. Defaults to `./[dir]/[name].[ext]`. diff --git a/docs/bundler/vs-esbuild.md b/docs/bundler/vs-esbuild.md index 41e19374252881..83dccc4ecb0d07 100644 --- a/docs/bundler/vs-esbuild.md +++ b/docs/bundler/vs-esbuild.md @@ -94,8 +94,8 @@ In Bun's CLI, simple boolean flags like `--minify` do not accept an argument. Ot --- - `--packages` -- n/a -- Not supported +- `--packages` +- No differences --- diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 01a228196e00e9..7dc3808ce30494 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -1516,6 +1516,7 @@ declare module "bun" { plugins?: BunPlugin[]; // manifest?: boolean; // whether to return manifest external?: string[]; + packages?: "bundle" | "external"; publicPath?: string; define?: Record; // origin?: string; // e.g. http://mydomain.com diff --git a/src/api/schema.zig b/src/api/schema.zig index 8ad90cfc476080..8bed348e9b1946 100644 --- a/src/api/schema.zig +++ b/src/api/schema.zig @@ -1684,6 +1684,9 @@ pub const Api = struct { /// conditions conditions: []const []const u8, + /// packages + packages: ?PackagesMode = null, + pub fn decode(reader: anytype) anyerror!TransformOptions { var this = std.mem.zeroes(TransformOptions); @@ -1771,6 +1774,9 @@ pub const Api = struct { 26 => { this.conditions = try reader.readArray([]const u8); }, + 27 => { + this.packages = try reader.readValue(PackagesMode); + }, else => { return error.InvalidMessage; }, @@ -1886,6 +1892,11 @@ pub const Api = struct { try writer.writeArray([]const u8, conditions); } + if (this.packages) |packages| { + try writer.writeFieldID(27); + try writer.writeValue([]const u8, packages); + } + try writer.endMessage(); } }; @@ -1908,6 +1919,20 @@ pub const Api = struct { } }; + pub const PackagesMode = enum(u8) { + /// bundle + bundle, + + /// external + external, + + _, + + pub fn jsonStringify(self: @This(), writer: anytype) !void { + return try writer.write(@tagName(self)); + } + }; + pub const FileHandle = struct { /// path path: []const u8, diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index c4b82934ede0b7..9a9ad0c11f6563 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -67,6 +67,7 @@ pub const JSBundler = struct { source_map: options.SourceMapOption = .none, public_path: OwnedString = OwnedString.initEmpty(bun.default_allocator), conditions: bun.StringSet = bun.StringSet.init(bun.default_allocator), + packages: options.PackagesOption = .bundle, pub const List = bun.StringArrayHashMapUnmanaged(Config); @@ -223,6 +224,10 @@ pub const JSBundler = struct { } } + if (try config.getOptionalEnum(globalThis, "packages", options.PackagesOption)) |packages| { + this.packages = packages; + } + if (try config.getOptionalEnum(globalThis, "format", options.Format)) |format| { switch (format) { .esm => {}, diff --git a/src/bun.js/api/JSTranspiler.zig b/src/bun.js/api/JSTranspiler.zig index 04a2a2d4750e11..966372a0d74964 100644 --- a/src/bun.js/api/JSTranspiler.zig +++ b/src/bun.js/api/JSTranspiler.zig @@ -586,6 +586,10 @@ fn transformOptionsFromJSC(globalObject: JSC.C.JSContextRef, temp_allocator: std } } + if (try object.getOptionalEnum(globalThis, "packages", options.PackagesOption)) |packages| { + transpiler.transform.packages = packages.toAPI(); + } + var tree_shaking: ?bool = null; if (object.getOptional(globalThis, "treeShaking", bool) catch return transpiler) |treeShaking| { tree_shaking = treeShaking; diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 4bb974b65e17a6..2fa06eb9cfd47d 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -1644,6 +1644,7 @@ pub const BundleV2 = struct { bundler.options.minify_identifiers = config.minify.identifiers; bundler.options.inlining = config.minify.syntax; bundler.options.source_map = config.source_map; + bundler.options.packages = config.packages; bundler.resolver.generation = generation; bundler.options.code_splitting = config.code_splitting; diff --git a/src/cli.zig b/src/cli.zig index 43bffb5e369be8..6c74ac63e16494 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -227,6 +227,7 @@ pub const Arguments = struct { clap.parseParam("--splitting Enable code splitting") catch unreachable, clap.parseParam("--public-path A prefix to be appended to any import paths in bundled code") catch unreachable, clap.parseParam("-e, --external ... Exclude module from transpilation (can use * wildcards). ex: -e react") catch unreachable, + clap.parseParam("--packages Add dependencies to bundle or keep them external. \"external\", \"bundle\" is supported. Defaults to \"bundle\".") catch unreachable, clap.parseParam("--entry-naming Customize entry point filenames. Defaults to \"[dir]/[name].[ext]\"") catch unreachable, clap.parseParam("--chunk-naming Customize chunk filenames. Defaults to \"[name]-[hash].[ext]\"") catch unreachable, clap.parseParam("--asset-naming Customize asset filenames. Defaults to \"[name]-[hash].[ext]\"") catch unreachable, @@ -696,6 +697,17 @@ pub const Arguments = struct { opts.external = externals; } + if (args.option("--packages")) |packages| { + if (strings.eqlComptime(packages, "bundle")) { + opts.packages = .bundle; + } else if (strings.eqlComptime(packages, "external")) { + opts.packages = .external; + } else { + Output.prettyErrorln("error: Invalid packages setting: \"{s}\"", .{packages}); + Global.crash(); + } + } + const TargetMatcher = strings.ExactSizeMatcher(8); if (args.option("--target")) |_target| brk: { if (comptime cmd == .BuildCommand) { diff --git a/src/options.zig b/src/options.zig index d228c73a6954bc..d7441061a63a2d 100644 --- a/src/options.zig +++ b/src/options.zig @@ -1377,6 +1377,31 @@ pub const SourceMapOption = enum { }); }; +pub const PackagesOption = enum { + bundle, + external, + + pub fn fromApi(packages: ?Api.PackagesMode) PackagesOption { + return switch (packages orelse .bundle) { + .external => .external, + .bundle => .bundle, + else => .bundle, + }; + } + + pub fn toAPI(packages: ?PackagesOption) Api.PackagesMode { + return switch (packages orelse .bundle) { + .external => .external, + .bundle => .bundle, + }; + } + + pub const Map = bun.ComptimeStringMap(PackagesOption, .{ + .{ "external", .external }, + .{ "bundle", .bundle }, + }); +}; + pub const OutputFormat = enum { preserve, @@ -1475,6 +1500,7 @@ pub const BundleOptions = struct { tree_shaking: bool = false, code_splitting: bool = false, source_map: SourceMapOption = SourceMapOption.none, + packages: PackagesOption = PackagesOption.bundle, disable_transpilation: bool = false, @@ -1737,6 +1763,8 @@ pub const BundleOptions = struct { opts.source_map = SourceMapOption.fromApi(transform.source_map orelse .none); + opts.packages = PackagesOption.fromApi(transform.packages orelse .bundle); + opts.tree_shaking = opts.target.isBun() or opts.production; opts.inlining = opts.tree_shaking; if (opts.inlining) diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 0ec56a6a7c52d3..315472252ec980 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -481,6 +481,13 @@ pub fn ResolveWatcher(comptime Context: type, comptime onWatch: anytype) type { }; } +fn isExternalModuleLike(import_path: string) bool { + if (strings.startsWith(import_path, ".") or strings.startsWith(import_path, "/") or strings.startsWith(import_path, "..")) { + return false; + } + return true; +} + pub const Resolver = struct { const ThisResolver = @This(); opts: options.BundleOptions, @@ -625,6 +632,9 @@ pub const Resolver = struct { } pub fn isExternalPattern(r: *ThisResolver, import_path: string) bool { + if (r.opts.packages == .external and isExternalModuleLike(import_path)) { + return true; + } for (r.opts.external.patterns) |pattern| { if (import_path.len >= pattern.prefix.len + pattern.suffix.len and (strings.startsWith( import_path, diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts index 4baa0d9186c2e2..45e0149fee954f 100644 --- a/test/bundler/bundler_edgecase.test.ts +++ b/test/bundler/bundler_edgecase.test.ts @@ -1309,6 +1309,31 @@ describe("bundler", () => { target: "bun", run: true, }); + itBundled("edgecase/PackageExternalDoNotBundleNodeModules", { + files: { + "/entry.ts": /* ts */ ` + import { a } from "foo"; + console.log(a); + `, + }, + packages: "external", + target: "bun", + runtimeFiles: { + "/node_modules/foo/index.js": `export const a = "Hello World";`, + "/node_modules/foo/package.json": /* json */ ` + { + "name": "foo", + "version": "2.0.0", + "main": "index.js" + } + `, + }, + run: { + stdout: ` + Hello World + `, + }, + }); // TODO(@paperdave): test every case of this. I had already tested it manually, but it may break later const requireTranspilationListESM = [ diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index 37ebb57267b7e4..ec6f4b3e6ee7e2 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -159,6 +159,8 @@ export interface BundlerTestInput { extensionOrder?: string[]; /** Replaces "{{root}}" with the file root */ external?: string[]; + /** Defaults to "bundle" */ + packages?: "bundle" | "external"; /** Defaults to "esm" */ format?: "esm" | "cjs" | "iife"; globalName?: string; @@ -400,6 +402,7 @@ function expectBundled( entryPointsRaw, env, external, + packages, files, format, globalName, @@ -621,6 +624,7 @@ function expectBundled( `--target=${target}`, // `--format=${format}`, external && external.map(x => ["--external", x]), + packages && ["--packages", packages], conditions && conditions.map(x => ["--conditions", x]), minifyIdentifiers && `--minify-identifiers`, minifySyntax && `--minify-syntax`, @@ -658,6 +662,7 @@ function expectBundled( minifyWhitespace && `--minify-whitespace`, globalName && `--global-name=${globalName}`, external && external.map(x => `--external:${x}`), + packages && ["--packages", packages], conditions && `--conditions=${conditions.join(",")}`, inject && inject.map(x => `--inject:${path.join(root, x)}`), define && Object.entries(define).map(([k, v]) => `--define:${k}=${v}`), @@ -927,6 +932,7 @@ function expectBundled( const buildConfig = { entrypoints: [...entryPaths, ...(entryPointsRaw ?? [])], external, + packages, minify: { whitespace: minifyWhitespace, identifiers: minifyIdentifiers,