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:
Jarred Sumner
2024-10-02 20:15:29 -07:00
committed by GitHub
parent 54e177e2f9
commit 94a656bc4f
3 changed files with 141 additions and 27 deletions

View File

@@ -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{

View File

@@ -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.

View 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()];