mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
Support bundling .node files in ESM & CJS when targeting Node.js (#14294)
Co-authored-by: Jarred-Sumner <Jarred-Sumner@users.noreply.github.com>
This commit is contained in:
@@ -3070,20 +3070,11 @@ pub const ParseTask = struct {
|
||||
return JSAst.init((try js_parser.newLazyExportAST(allocator, bundler.options.define, opts, log, root, &source, "")).?);
|
||||
},
|
||||
.napi => {
|
||||
if (bundler.options.target == .node) {
|
||||
if (bundler.options.target == .browser) {
|
||||
log.addError(
|
||||
null,
|
||||
Logger.Loc.Empty,
|
||||
"TODO: implement .node loader for Node.js target",
|
||||
) catch bun.outOfMemory();
|
||||
return error.ParserError;
|
||||
}
|
||||
|
||||
if (bundler.options.target != .bun) {
|
||||
log.addError(
|
||||
null,
|
||||
Logger.Loc.Empty,
|
||||
"To load .node files, set target to \"bun\"",
|
||||
"Loading .node files won't work in the browser. Make sure to set target to \"bun\" or \"node\"",
|
||||
) catch bun.outOfMemory();
|
||||
return error.ParserError;
|
||||
}
|
||||
@@ -3091,27 +3082,20 @@ pub const ParseTask = struct {
|
||||
const unique_key = std.fmt.allocPrint(allocator, "{any}A{d:0>8}", .{ bun.fmt.hexIntLower(unique_key_prefix), source.index.get() }) catch unreachable;
|
||||
// This injects the following code:
|
||||
//
|
||||
// import.meta.require(unique_key)
|
||||
// require(unique_key)
|
||||
//
|
||||
const import_path = Expr.init(E.String, E.String{
|
||||
.data = unique_key,
|
||||
}, Logger.Loc{ .start = 0 });
|
||||
|
||||
// TODO: e_require_string
|
||||
const import_meta = Expr.init(E.ImportMeta, E.ImportMeta{}, Logger.Loc{ .start = 0 });
|
||||
const require_property = Expr.init(E.Dot, E.Dot{
|
||||
.target = import_meta,
|
||||
.name_loc = Logger.Loc.Empty,
|
||||
.name = "require",
|
||||
}, Logger.Loc{ .start = 0 });
|
||||
const require_args = allocator.alloc(Expr, 1) catch unreachable;
|
||||
require_args[0] = import_path;
|
||||
const require_call = Expr.init(E.Call, E.Call{
|
||||
.target = require_property,
|
||||
|
||||
const root = Expr.init(E.Call, E.Call{
|
||||
.target = .{ .data = .{ .e_require_call_target = {} }, .loc = .{ .start = 0 } },
|
||||
.args = BabyList(Expr).init(require_args),
|
||||
}, Logger.Loc{ .start = 0 });
|
||||
|
||||
const root = require_call;
|
||||
unique_key_for_additional_file.* = unique_key;
|
||||
return JSAst.init((try js_parser.newLazyExportAST(allocator, bundler.options.define, opts, log, root, &source, "")).?);
|
||||
},
|
||||
@@ -5221,6 +5205,21 @@ pub const LinkerContext = struct {
|
||||
expr,
|
||||
);
|
||||
try this.graph.generateSymbolImportAndUse(source_index, 0, module_ref, 1, Index.init(source_index));
|
||||
|
||||
// If this is a .napi addon and it's not node, we need to generate a require() call to the runtime
|
||||
if (expr.data == .e_call and expr.data.e_call.target.data == .e_require_call_target and
|
||||
// if it's commonjs, use require()
|
||||
this.options.output_format != .cjs and
|
||||
// if it's esm and bun, use import.meta.require(). the code for __require is not injected into the bundle.
|
||||
!this.options.target.isBun())
|
||||
{
|
||||
this.graph.generateRuntimeSymbolImportAndUse(
|
||||
source_index,
|
||||
Index.part(1),
|
||||
"__require",
|
||||
1,
|
||||
) catch {};
|
||||
}
|
||||
},
|
||||
else => {
|
||||
// Otherwise, generate ES6 export statements. These are added as additional
|
||||
@@ -5941,7 +5940,6 @@ pub const LinkerContext = struct {
|
||||
// generating a CommonJS output file, since it won't exist otherwise.
|
||||
// Disabled for target bun because `import.meta.require` will be inlined.
|
||||
if (shouldCallRuntimeRequire(output_format) and !this.resolver.opts.target.isBun()) {
|
||||
record.calls_runtime_require = true;
|
||||
runtime_require_uses += 1;
|
||||
}
|
||||
|
||||
@@ -7555,7 +7553,7 @@ pub const LinkerContext = struct {
|
||||
var runtime_members = &runtime_scope.members;
|
||||
const toCommonJSRef = c.graph.symbols.follow(runtime_members.get("__toCommonJS").?.ref);
|
||||
const toESMRef = c.graph.symbols.follow(runtime_members.get("__toESM").?.ref);
|
||||
const runtimeRequireRef = if (c.resolver.opts.target.isBun()) null else c.graph.symbols.follow(runtime_members.get("__require").?.ref);
|
||||
const runtimeRequireRef = if (c.resolver.opts.target.isBun() or c.options.output_format == .cjs) null else c.graph.symbols.follow(runtime_members.get("__require").?.ref);
|
||||
|
||||
{
|
||||
const print_options = js_printer.Options{
|
||||
|
||||
@@ -117,8 +117,6 @@ pub const ImportRecord = struct {
|
||||
|
||||
is_internal: bool = false,
|
||||
|
||||
calls_runtime_require: bool = false,
|
||||
|
||||
/// Sometimes the parser creates an import record and decides it isn't needed.
|
||||
/// For example, TypeScript code may have import statements that later turn
|
||||
/// out to be type-only imports after analyzing the whole file.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { spawnSync } from "bun";
|
||||
import { beforeAll, describe, expect, it } from "bun:test";
|
||||
import { bunEnv, bunExe } from "harness";
|
||||
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
describe("napi", () => {
|
||||
@@ -18,6 +18,124 @@ describe("napi", () => {
|
||||
throw new Error("build failed");
|
||||
}
|
||||
});
|
||||
|
||||
describe.each(["esm", "cjs"])("bundle .node files to %s via", format => {
|
||||
describe.each(["node", "bun"])("target %s", target => {
|
||||
it("Bun.build", async () => {
|
||||
const dir = tempDirWithFiles("node-file-cli", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "napi-app",
|
||||
version: "1.0.0",
|
||||
type: format === "esm" ? "module" : "commonjs",
|
||||
}),
|
||||
});
|
||||
const build = spawnSync({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"build",
|
||||
"--target",
|
||||
target,
|
||||
"--outdir",
|
||||
dir,
|
||||
"--format=" + format,
|
||||
join(__dirname, "napi-app/main.js"),
|
||||
],
|
||||
cwd: join(__dirname, "napi-app"),
|
||||
env: bunEnv,
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
});
|
||||
expect(build.success).toBeTrue();
|
||||
|
||||
for (let exec of target === "bun" ? [bunExe()] : [bunExe(), "node"]) {
|
||||
const result = spawnSync({
|
||||
cmd: [exec, join(dir, "main.js"), "self"],
|
||||
env: bunEnv,
|
||||
stdin: "inherit",
|
||||
stderr: "inherit",
|
||||
stdout: "pipe",
|
||||
});
|
||||
const stdout = result.stdout.toString().trim();
|
||||
expect(stdout).toBe("hello world!");
|
||||
expect(result.success).toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
if (target === "bun") {
|
||||
it("should work with --compile", async () => {
|
||||
const dir = tempDirWithFiles("napi-app-compile-" + format, {
|
||||
"package.json": JSON.stringify({
|
||||
name: "napi-app",
|
||||
version: "1.0.0",
|
||||
type: format === "esm" ? "module" : "commonjs",
|
||||
}),
|
||||
});
|
||||
|
||||
const exe = join(dir, "main" + (process.platform === "win32" ? ".exe" : ""));
|
||||
const build = spawnSync({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"build",
|
||||
"--target=" + target,
|
||||
"--format=" + format,
|
||||
"--compile",
|
||||
join(__dirname, "napi-app", "main.js"),
|
||||
],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
});
|
||||
expect(build.success).toBeTrue();
|
||||
|
||||
const result = spawnSync({
|
||||
cmd: [exe, "self"],
|
||||
env: bunEnv,
|
||||
stdin: "inherit",
|
||||
stderr: "inherit",
|
||||
stdout: "pipe",
|
||||
});
|
||||
const stdout = result.stdout.toString().trim();
|
||||
|
||||
expect(stdout).toBe("hello world!");
|
||||
expect(result.success).toBeTrue();
|
||||
});
|
||||
}
|
||||
|
||||
it("`bun build`", async () => {
|
||||
const dir = tempDirWithFiles("node-file-build", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "napi-app",
|
||||
version: "1.0.0",
|
||||
type: format === "esm" ? "module" : "commonjs",
|
||||
}),
|
||||
});
|
||||
const build = await Bun.build({
|
||||
entrypoints: [join(__dirname, "napi-app/main.js")],
|
||||
outdir: dir,
|
||||
target,
|
||||
format,
|
||||
});
|
||||
|
||||
expect(build.logs).toBeEmpty();
|
||||
|
||||
for (let exec of target === "bun" ? [bunExe()] : [bunExe(), "node"]) {
|
||||
const result = spawnSync({
|
||||
cmd: [exec, join(dir, "main.js"), "self"],
|
||||
env: bunEnv,
|
||||
stdin: "inherit",
|
||||
stderr: "inherit",
|
||||
stdout: "pipe",
|
||||
});
|
||||
const stdout = result.stdout.toString().trim();
|
||||
|
||||
expect(stdout).toBe("hello world!");
|
||||
expect(result.success).toBeTrue();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue_7685", () => {
|
||||
it("works", () => {
|
||||
const args = [...Array(20).keys()];
|
||||
|
||||
Reference in New Issue
Block a user