diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 01a7205d9d..780c964a9f 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -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{ diff --git a/src/import_record.zig b/src/import_record.zig index 7e5ceba4a1..9125d18f35 100644 --- a/src/import_record.zig +++ b/src/import_record.zig @@ -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. diff --git a/test/napi/napi.test.ts b/test/napi/napi.test.ts index df6bc413a9..0e09e6fcfe 100644 --- a/test/napi/napi.test.ts +++ b/test/napi/napi.test.ts @@ -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()];