Compare commits

...

6 Commits

Author SHA1 Message Date
autofix-ci[bot]
715dd5bfc3 [autofix.ci] apply automated fixes 2025-09-10 21:01:46 +00:00
Claude Bot
7260d5fe22 Fix nativefill.zig import order
Move imports to the top of the file before variable definitions to fix compilation order.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-10 21:00:08 +00:00
Claude Bot
53d49a1dc2 Fix nativefill test expectations
The tests were incorrectly expecting `Bun.stripAnsi` but the actual API is `Bun.stripANSI` with capital ANSI. Updated all test expectations to match the correct API name.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-10 20:54:21 +00:00
autofix-ci[bot]
0de4fcbc05 [autofix.ci] apply automated fixes 2025-09-10 19:44:40 +00:00
Claude Bot
db66f28fae Fix nativefill: Use correct Bun.stripANSI (capital ANSI)
The nativefill feature now correctly:
- Replaces strip-ansi with Bun.stripANSI (not stripAnsi)
- Replaces string-width with Bun.stringWidth
- Replaces better-sqlite3 with bun:sqlite Database export

Tested and verified working with actual bundle output.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-10 11:08:13 +00:00
Claude Bot
3f76ceed97 feat: add --nativefill option to Bun.build
Implements --nativefill flag that replaces imports of packages with Bun's native implementations when target is "bun".

Replacements:
- strip-ansi -> Bun.stripAnsi
- string-width -> Bun.stringWidth
- better-sqlite3 -> bun:sqlite

This feature is opt-in and disabled by default. It only works when target is set to "bun".

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-10 10:46:12 +00:00
11 changed files with 425 additions and 0 deletions

View File

@@ -1714,6 +1714,9 @@ pub const api = struct {
/// ignore_dce_annotations
ignore_dce_annotations: bool,
/// nativefill
nativefill: bool = false,
/// e.g.:
/// [serve.static]
/// plugins = ["tailwindcss"]

View File

@@ -38,6 +38,7 @@ pub const JSBundler = struct {
env_prefix: OwnedString = OwnedString.initEmpty(bun.default_allocator),
tsconfig_override: OwnedString = OwnedString.initEmpty(bun.default_allocator),
compile: ?CompileOptions = null,
nativefill: bool = false,
pub const CompileOptions = struct {
compile_target: CompileTarget = .{},
@@ -301,6 +302,10 @@ pub const JSBundler = struct {
this.no_macros = !macros_flag;
}
if (try config.getBooleanLoose(globalThis, "nativefill")) |nativefill| {
this.nativefill = nativefill;
}
if (try config.getBooleanLoose(globalThis, "bytecode")) |bytecode| {
this.bytecode = bytecode;

View File

@@ -636,6 +636,22 @@ fn getCodeForParseTaskWithoutPlugins(
const trace = bun.perf.trace("Bundler.readFile");
defer trace.end();
// Check if this is a nativefill virtual path
if (strings.hasPrefixComptime(file_path.text, "/bun-nativefill/")) {
const NativefillModules = @import("../nativefill.zig");
// Extract module name from path like "/bun-nativefill/strip-ansi.js"
const module_name = file_path.text["/bun-nativefill/".len..];
const dot_index = std.mem.indexOfScalar(u8, module_name, '.') orelse module_name.len;
const base_name = module_name[0..dot_index];
if (NativefillModules.Map.get(base_name)) |source| {
break :brk .{
.contents = source.contents,
.fd = bun.invalid_fd,
};
}
}
if (strings.eqlComptime(file_path.namespace, "node")) lookup_builtin: {
if (task.ctx.framework) |f| {
if (f.built_in_modules.get(file_path.text)) |file| {

View File

@@ -1861,6 +1861,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.nativefill = config.nativefill;
if (transpiler.options.compile) {
// Emitting DCE annotations is nonsensical in --compile.

View File

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

View File

@@ -67,6 +67,7 @@ pub const transpiler_params_ = [_]ParamType{
clap.parseParam("--drop <STR>... Remove function calls, e.g. --drop=console removes all console.* calls.") catch unreachable,
clap.parseParam("-l, --loader <STR>... Parse files with .ext:loader, e.g. --loader .js:jsx. Valid loaders: js, jsx, ts, tsx, json, toml, text, file, wasm, napi") catch unreachable,
clap.parseParam("--no-macros Disable macros from being executed in the bundler, transpiler and runtime") catch unreachable,
clap.parseParam("--nativefill Replace imports of packages with Bun's native implementations when available (requires target to be 'bun')") catch unreachable,
clap.parseParam("--jsx-factory <STR> Changes the function called when compiling JSX elements using the classic JSX runtime") catch unreachable,
clap.parseParam("--jsx-fragment <STR> Changes the function called when compiling JSX fragments") catch unreachable,
clap.parseParam("--jsx-import-source <STR> Declares the module specifier to be used for importing the jsx and jsxs factory functions. Default: \"react\"") catch unreachable,
@@ -1224,6 +1225,10 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
ctx.debug.macros = .{ .disable = {} };
}
if (args.flag("--nativefill")) {
ctx.bundler_options.nativefill = true;
}
opts.output_dir = output_dir;
if (output_file != null)
ctx.debug.output_file = output_file.?;

View File

@@ -77,6 +77,7 @@ pub const BuildCommand = struct {
this_transpiler.options.minify_identifiers = ctx.bundler_options.minify_identifiers;
this_transpiler.options.emit_dce_annotations = ctx.bundler_options.emit_dce_annotations;
this_transpiler.options.ignore_dce_annotations = ctx.bundler_options.ignore_dce_annotations;
this_transpiler.options.nativefill = ctx.bundler_options.nativefill;
this_transpiler.options.banner = ctx.bundler_options.banner;
this_transpiler.options.footer = ctx.bundler_options.footer;

12
src/nativefill.zig Normal file
View File

@@ -0,0 +1,12 @@
const strip_ansi_source = logger.Source.initPathString("/bun-nativefill/strip-ansi.js", "export default Bun.stripANSI;");
const string_width_source = logger.Source.initPathString("/bun-nativefill/string-width.js", "export default Bun.stringWidth;");
const better_sqlite3_source = logger.Source.initPathString("/bun-nativefill/better-sqlite3.js", "export { Database as default } from 'bun:sqlite';");
pub const Map = bun.ComptimeStringMap(*const logger.Source, .{
.{ "strip-ansi", &strip_ansi_source },
.{ "string-width", &string_width_source },
.{ "better-sqlite3", &better_sqlite3_source },
});
const bun = @import("bun");
const logger = bun.logger;

View File

@@ -1770,6 +1770,7 @@ pub const BundleOptions = struct {
macro_remap: MacroRemap = MacroRemap{},
no_macros: bool = false,
nativefill: bool = false,
conditions: ESMConditions = undefined,
tree_shaking: bool = false,

View File

@@ -1217,6 +1217,22 @@ pub const Resolver = struct {
}
if (check_package) {
// Check for nativefill replacements when enabled and target is bun
if (r.opts.nativefill and r.opts.target.isBun()) {
if (NativefillModules.Map.get(import_path)) |source| {
// Return a virtual module with the nativefill source
result.path_pair.primary = Fs.Path.init(source.path.text);
result.is_from_node_modules = true;
result.diff_case = null;
result.file_fd = .invalid;
result.dirname_fd = .invalid;
result.module_type = .esm;
result.primary_side_effects_data = .no_side_effects__pure_data;
result.is_external = false;
return .{ .success = result };
}
}
if (r.opts.polyfill_node_globals) {
const had_node_prefix = strings.hasPrefixComptime(import_path, "node:");
const import_path_without_node_prefix = if (had_node_prefix) import_path["node:".len..] else import_path;
@@ -4345,6 +4361,7 @@ const string = []const u8;
const Dependency = @import("../install/dependency.zig");
const DotEnv = @import("../env_loader.zig");
const NativefillModules = @import("../nativefill.zig");
const NodeFallbackModules = @import("../node_fallbacks.zig");
const ResolvePath = @import("./resolve_path.zig");
const ast = @import("../import_record.zig");

View File

@@ -0,0 +1,363 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import path from "path";
test("nativefill replaces strip-ansi with Bun.stripANSI when target is bun", async () => {
using dir = tempDir("nativefill-strip-ansi", {
"index.js": `
import stripAnsi from 'strip-ansi';
console.log(stripAnsi('\\x1b[31mHello\\x1b[0m'));
`,
"package.json": JSON.stringify({
name: "test-app",
dependencies: {
"strip-ansi": "7.0.0",
},
}),
});
const outdir = path.join(String(dir), "out");
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "./index.js", "--outdir", outdir, "--target", "bun", "--nativefill"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// Read the output file
const outputFile = path.join(outdir, "index.js");
const output = await Bun.file(outputFile).text();
// Should use Bun.stripANSI, not the npm package
expect(output).toContain("Bun.stripANSI");
expect(output).not.toContain("node_modules");
});
test("nativefill replaces string-width with Bun.stringWidth when target is bun", async () => {
using dir = tempDir("nativefill-string-width", {
"index.js": `
import stringWidth from 'string-width';
console.log(stringWidth('hello'));
`,
"package.json": JSON.stringify({
name: "test-app",
dependencies: {
"string-width": "5.0.0",
},
}),
});
const outdir = path.join(String(dir), "out");
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "./index.js", "--outdir", outdir, "--target", "bun", "--nativefill"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// Read the output file
const outputFile = path.join(outdir, "index.js");
const output = await Bun.file(outputFile).text();
// Should use Bun.stringWidth, not the npm package
expect(output).toContain("Bun.stringWidth");
expect(output).not.toContain("node_modules");
});
test("nativefill replaces better-sqlite3 with bun:sqlite when target is bun", async () => {
using dir = tempDir("nativefill-better-sqlite3", {
"index.js": `
import Database from 'better-sqlite3';
const db = new Database(':memory:');
console.log(db);
`,
"package.json": JSON.stringify({
name: "test-app",
dependencies: {
"better-sqlite3": "9.0.0",
},
}),
});
const outdir = path.join(String(dir), "out");
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "./index.js", "--outdir", outdir, "--target", "bun", "--nativefill"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// Read the output file
const outputFile = path.join(outdir, "index.js");
const output = await Bun.file(outputFile).text();
// Should use bun:sqlite, not the npm package
expect(output).toContain("bun:sqlite");
expect(output).not.toContain("node_modules");
});
test("nativefill is disabled by default", async () => {
using dir = tempDir("nativefill-disabled", {
"index.js": `
import stripAnsi from 'strip-ansi';
console.log(stripAnsi('\\x1b[31mHello\\x1b[0m'));
`,
"package.json": JSON.stringify({
name: "test-app",
dependencies: {
"strip-ansi": "7.0.0",
},
}),
});
// First install the dependency
await using installProc = Bun.spawn({
cmd: [bunExe(), "install"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
await installProc.exited;
const outdir = path.join(String(dir), "out");
// Build without --nativefill flag
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "./index.js", "--outdir", outdir, "--target", "bun"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// Read the output file
const outputFile = path.join(outdir, "index.js");
const output = await Bun.file(outputFile).text();
// Should NOT use Bun.stripANSI since nativefill is disabled
expect(output).not.toContain("Bun.stripANSI");
// Should contain the actual module import
expect(output).toContain("strip-ansi");
});
test("nativefill fails when target is not bun", async () => {
using dir = tempDir("nativefill-wrong-target", {
"index.js": `
import stripAnsi from 'strip-ansi';
console.log(stripAnsi('\\x1b[31mHello\\x1b[0m'));
`,
"package.json": JSON.stringify({
name: "test-app",
dependencies: {
"strip-ansi": "7.0.0",
},
}),
});
// First install the dependency
await using installProc = Bun.spawn({
cmd: [bunExe(), "install"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
await installProc.exited;
const outdir = path.join(String(dir), "out");
// Try building with --nativefill but target is browser
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "./index.js", "--outdir", outdir, "--target", "browser", "--nativefill"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// Read the output file
const outputFile = path.join(outdir, "index.js");
const output = await Bun.file(outputFile).text();
// Should NOT use Bun.stripANSI when target is not bun
expect(output).not.toContain("Bun.stripANSI");
// Should contain the actual module import
expect(output).toContain("strip-ansi");
});
test("nativefill works with multiple imports", async () => {
using dir = tempDir("nativefill-multiple", {
"index.js": `
import stripAnsi from 'strip-ansi';
import stringWidth from 'string-width';
import Database from 'better-sqlite3';
console.log(stripAnsi('\\x1b[31mHello\\x1b[0m'));
console.log(stringWidth('hello'));
const db = new Database(':memory:');
`,
"package.json": JSON.stringify({
name: "test-app",
dependencies: {
"strip-ansi": "7.0.0",
"string-width": "5.0.0",
"better-sqlite3": "9.0.0",
},
}),
});
const outdir = path.join(String(dir), "out");
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "./index.js", "--outdir", outdir, "--target", "bun", "--nativefill"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// Read the output file
const outputFile = path.join(outdir, "index.js");
const output = await Bun.file(outputFile).text();
// Should use all Bun native implementations
expect(output).toContain("Bun.stripANSI");
expect(output).toContain("Bun.stringWidth");
expect(output).toContain("bun:sqlite");
expect(output).not.toContain("node_modules");
});
test("nativefill works with JS API", async () => {
using dir = tempDir("nativefill-js-api", {
"build.js": `
await Bun.build({
entrypoints: ["./index.js"],
outdir: "./out",
target: "bun",
nativefill: true,
});
`,
"index.js": `
import stripAnsi from 'strip-ansi';
console.log(stripAnsi('test'));
`,
"package.json": JSON.stringify({
name: "test-app",
dependencies: {
"strip-ansi": "7.0.0",
},
}),
});
// Run the build script
await using proc = Bun.spawn({
cmd: [bunExe(), "build.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// Read the output file
const outputFile = path.join(String(dir), "out", "index.js");
const output = await Bun.file(outputFile).text();
// Should use Bun.stripANSI
expect(output).toContain("Bun.stripANSI");
expect(output).not.toContain("node_modules");
});
test("nativefill correctly replaces imports in output", async () => {
using dir = tempDir("nativefill-output-check", {
"test.js": `
import stripAnsi from 'strip-ansi';
import stringWidth from 'string-width';
import Database from 'better-sqlite3';
// Use the imports
const result = stripAnsi('test');
const width = stringWidth('hello');
const db = new Database(':memory:');
`,
"package.json": JSON.stringify({
name: "test-app",
dependencies: {
"strip-ansi": "7.0.0",
"string-width": "5.0.0",
"better-sqlite3": "9.0.0",
},
}),
});
const outdir = path.join(String(dir), "out");
// Build with nativefill
await using buildProc = Bun.spawn({
cmd: [bunExe(), "build", "./test.js", "--outdir", outdir, "--target", "bun", "--nativefill"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [buildStdout, buildStderr, buildExitCode] = await Promise.all([
buildProc.stdout.text(),
buildProc.stderr.text(),
buildProc.exited,
]);
expect(buildExitCode).toBe(0);
// Check the output file contains the replacements
const outputFile = path.join(outdir, "test.js");
const output = await Bun.file(outputFile).text();
// Verify all replacements are in the output
expect(output).toContain("Bun.stripANSI");
expect(output).toContain("Bun.stringWidth");
expect(output).toContain("bun:sqlite");
// Verify the npm packages are not referenced as imports
expect(output).not.toContain("node_modules");
// The names might appear in comments, but shouldn't be imported from npm packages
expect(output).not.toMatch(/from ["']strip-ansi["']/);
expect(output).not.toMatch(/from ["']string-width["']/);
expect(output).not.toMatch(/from ["']better-sqlite3["']/);
});