Compare commits

...

4 Commits

Author SHA1 Message Date
Don Isaac
5227ad5533 Merge branch 'main' into don/fix/relative-bunfig-preloads 2025-04-14 11:45:14 -07:00
Don Isaac
888f805cea minor cleanup 2025-04-09 13:26:38 -07:00
Don Isaac
34df2681ad more test cases 2025-04-09 13:24:24 -07:00
Don Isaac
0435fe39e8 fxi(bunfig): resolve preloads relative to bunfig.toml 2025-04-09 13:21:49 -07:00
9 changed files with 130 additions and 45 deletions

View File

@@ -43,7 +43,7 @@ pub fn buildCommand(ctx: bun.CLI.Command.Context) !void {
vm.jsc = vm.global.vm();
vm.event_loop.ensureWaker();
const b = &vm.transpiler;
vm.preload = ctx.preloads;
vm.preload = ctx.preloads.items;
vm.argv = ctx.passthrough;
vm.arena = &arena;
vm.allocator = arena.allocator();

View File

@@ -87,6 +87,7 @@ const Lock = bun.Mutex;
const Async = bun.Async;
const Ordinal = bun.Ordinal;
const Preload = bun.CLI.Command.ContextData.Preload;
pub const OpaqueCallback = *const fn (current: ?*anyopaque) callconv(.C) void;
pub fn OpaqueWrap(comptime Context: type, comptime Function: fn (this: *Context) void) OpaqueCallback {
@@ -777,7 +778,7 @@ pub const VirtualMachine = struct {
timer: Bun.Timer.All,
event_loop_handle: ?*PlatformEventLoop = null,
pending_unref_counter: i32 = 0,
preload: []const string = &[_][]const u8{},
preload: []const Preload = &[_]Preload{},
unhandled_pending_rejection_to_capture: ?*JSValue = null,
standalone_module_graph: ?*bun.StandaloneModuleGraph = null,
smol: bool = false,
@@ -2974,8 +2975,9 @@ pub const VirtualMachine = struct {
for (this.preload) |preload| {
var result = switch (this.transpiler.resolver.resolveAndAutoInstall(
this.transpiler.fs.top_level_dir,
normalizeSource(preload),
// FIXME: should be defaulting to cwd
preload.root_dir orelse this.transpiler.fs.top_level_dir,
normalizeSource(preload.target),
.stmt,
if (this.standalone_module_graph == null) .read_only else .disable,
)) {
@@ -2988,7 +2990,8 @@ pub const VirtualMachine = struct {
"{s} resolving preload {}",
.{
@errorName(e),
bun.fmt.formatJSONStringLatin1(preload),
// FIXME: we cannot assume latin1. Windows uses UTF-16 for paths.
bun.fmt.formatJSONStringLatin1(preload.target),
},
) catch unreachable;
return e;
@@ -3000,7 +3003,8 @@ pub const VirtualMachine = struct {
this.allocator,
"preload not found {}",
.{
bun.fmt.formatJSONStringLatin1(preload),
// FIXME: we cannot assume latin1. Windows uses UTF-16 for paths.
bun.fmt.formatJSONStringLatin1(preload.target),
},
) catch unreachable;
return error.ModuleNotFound;

View File

@@ -6,6 +6,7 @@ const std = @import("std");
const JSValue = JSC.JSValue;
const Async = bun.Async;
const WTFStringImpl = @import("../string.zig").WTFStringImpl;
const Preload = bun.CLI.Command.ContextData.Preload;
const Bool = std.atomic.Value(bool);
@@ -22,7 +23,7 @@ pub const WebWorker = struct {
/// Already resolved.
specifier: []const u8 = "",
preloads: [][]const u8 = &.{},
preloads: []const Preload = &[_]Preload{},
store_fd: bool = false,
arena: ?bun.MimallocArena = null,
name: [:0]const u8 = "Worker",
@@ -209,17 +210,18 @@ pub const WebWorker = struct {
return null;
};
var preloads = std.ArrayList([]const u8).initCapacity(bun.default_allocator, preload_modules_len) catch bun.outOfMemory();
var preloads = std.ArrayList(Preload).initCapacity(bun.default_allocator, preload_modules_len) catch bun.outOfMemory();
for (preload_modules) |module| {
const utf8_slice = module.toUTF8(bun.default_allocator);
defer utf8_slice.deinit();
if (resolveEntryPointSpecifier(parent, utf8_slice.slice(), error_message, &temp_log)) |preload| {
preloads.append(bun.default_allocator.dupe(u8, preload) catch bun.outOfMemory()) catch bun.outOfMemory();
const target = bun.default_allocator.dupe(u8, preload) catch bun.outOfMemory();
preloads.append(Preload.initAbsolute(target)) catch bun.outOfMemory();
}
if (!error_message.isEmpty()) {
for (preloads.items) |preload| {
bun.default_allocator.free(preload);
bun.default_allocator.free(preload.target);
}
preloads.deinit();
return null;
@@ -326,7 +328,7 @@ pub const WebWorker = struct {
this.parent_poll_ref.unrefConcurrently(this.parent);
bun.default_allocator.free(this.specifier);
for (this.preloads) |preload| {
bun.default_allocator.free(preload);
bun.default_allocator.free(preload.target);
}
bun.default_allocator.free(this.preloads);
bun.default_allocator.destroy(this);

View File

@@ -77,7 +77,7 @@ pub const Run = struct {
var vm = run.vm;
var b = &vm.transpiler;
vm.preload = ctx.preloads;
vm.preload = ctx.preloads.items;
vm.argv = ctx.passthrough;
vm.arena = &run.arena;
vm.allocator = arena.allocator();
@@ -216,7 +216,7 @@ pub const Run = struct {
var vm = run.vm;
var b = &vm.transpiler;
vm.preload = ctx.preloads;
vm.preload = ctx.preloads.items;
vm.argv = ctx.passthrough;
vm.arena = &run.arena;
vm.allocator = arena.allocator();

View File

@@ -28,6 +28,7 @@ pub const BundlePackageOverride = bun.StringArrayHashMapUnmanaged(options.Bundle
const LoaderMap = bun.StringArrayHashMapUnmanaged(options.Loader);
const JSONParser = bun.JSON;
const Command = @import("cli.zig").Command;
const Preload = Command.ContextData.Preload;
const TOML = @import("./toml/toml_parser.zig").TOML;
// TODO: replace Api.TransformOptions with Bunfig
@@ -50,6 +51,9 @@ pub const Bunfig = struct {
allocator: std.mem.Allocator,
bunfig: *Api.TransformOptions,
ctx: Command.Context,
/// Absolute path to bunfig's parent dir.
/// Lazy-loaded. Derived from `source.path`.
parent_dir: ?[]const u8 = null,
fn addError(this: *Parser, loc: logger.Loc, comptime text: string) !void {
this.log.addErrorOpts(text, .{
@@ -69,6 +73,25 @@ pub const Bunfig = struct {
return error.@"Invalid Bunfig";
}
/// Lazy-load the parent directory of the source file. Result is both
/// cached in `.parent_dir` and returned.
fn loadParentDir(this: *Parser) ![]const u8 {
// Caller is responsible for lazy-load check
bun.debugAssert(this.parent_dir == null);
var source = this.source.path.sourceDir();
if (!std.fs.path.isAbsolute(source)) {
const cwd = try bun.getcwdAlloc(this.allocator);
defer this.allocator.free(cwd);
const absolute = bun.path.join(&[_]bun.string{ cwd, source }, .auto);
source = try this.allocator.dupe(u8, absolute);
} else {
// arena is reset before entering entrypoint, clobbering source path.
source = try this.allocator.dupe(u8, source);
}
this.parent_dir = source;
return source;
}
fn parseRegistryURLString(this: *Parser, str: *js_ast.E.String) !Api.NpmRegistry {
const url = URL.parse(str.data);
var registry = std.mem.zeroes(Api.NpmRegistry);
@@ -154,21 +177,22 @@ pub const Bunfig = struct {
allocator: std.mem.Allocator,
expr: js_ast.Expr,
) !void {
const parent_dir = if (this.parent_dir) |p| p else try this.loadParentDir();
var preloads = &this.ctx.preloads;
if (expr.asArray()) |array_| {
var array = array_;
var preloads = try std.ArrayList(string).initCapacity(allocator, array.array.items.len);
errdefer preloads.deinit();
preloads.clearRetainingCapacity();
try preloads.ensureTotalCapacityPrecise(this.ctx.allocator, array.array.items.len);
while (array.next()) |item| {
try this.expectString(item);
if (item.data.e_string.len() > 0)
preloads.appendAssumeCapacity(try item.data.e_string.string(allocator));
if (item.data.e_string.len() == 0) continue;
const target = try item.data.e_string.string(allocator);
preloads.appendAssumeCapacity(Preload.initRelative(parent_dir, target));
}
this.ctx.preloads = preloads.items;
} else if (expr.data == .e_string) {
if (expr.data.e_string.len() > 0) {
var preloads = try allocator.alloc(string, 1);
preloads[0] = try expr.data.e_string.string(allocator);
this.ctx.preloads = preloads;
const preload = try expr.data.e_string.string(allocator);
try preloads.append(this.ctx.allocator, Preload.initRelative(parent_dir, preload));
}
} else if (expr.data != .e_null) {
try this.addError(expr.loc, "Expected preload to be an array");

View File

@@ -767,23 +767,12 @@ pub const Arguments = struct {
}
}
if (ctx.preloads.len > 0 and (preloads.len > 0 or preloads2.len > 0)) {
var all = std.ArrayList(string).initCapacity(ctx.allocator, ctx.preloads.len + preloads.len + preloads2.len) catch unreachable;
all.appendSliceAssumeCapacity(ctx.preloads);
all.appendSliceAssumeCapacity(preloads);
all.appendSliceAssumeCapacity(preloads2);
ctx.preloads = all.items;
} else if (preloads.len > 0) {
if (preloads2.len > 0) {
var all = std.ArrayList(string).initCapacity(ctx.allocator, preloads.len + preloads2.len) catch unreachable;
all.appendSliceAssumeCapacity(preloads);
all.appendSliceAssumeCapacity(preloads2);
ctx.preloads = all.items;
} else {
ctx.preloads = preloads;
}
} else if (preloads2.len > 0) {
ctx.preloads = preloads2;
ctx.preloads.ensureTotalCapacityPrecise(ctx.allocator, ctx.preloads.items.len + preloads.len + preloads2.len) catch bun.outOfMemory();
for (preloads) |preload| {
ctx.preloads.appendAssumeCapacity(Command.ContextData.Preload.initCwd(preload));
}
for (preloads2) |preload| {
ctx.preloads.appendAssumeCapacity(Command.ContextData.Preload.initCwd(preload));
}
if (args.option("--print")) |script| {
@@ -1568,7 +1557,7 @@ pub const Command = struct {
filters: []const []const u8 = &.{},
preloads: []const string = &.{},
preloads: std.ArrayListUnmanaged(Preload) = .{},
has_loaded_global_config: bool = false,
pub const BundlerOptions = struct {
@@ -1611,6 +1600,56 @@ pub const Command = struct {
windows_icon: ?[]const u8 = null,
};
/// Preloads are files or plugins that should be run before the main script.
/// Used by run, test, and build.
///
/// Preloads have the semantics to `import(target)` from a JS/TS file in
/// `root_dir`. TODO: allow relative paths that do not have a leading `./`.
pub const Preload = struct {
/// Directory to resolve preload files from. `null` means CWD (_not_ project root).
root_dir: ?[]const u8,
/// The filepath/import specifier being preloaded. Path may be relative.
target: []const u8,
/// Create a `Preload` from an absolute path.
pub fn initAbsolute(target: []const u8) Preload {
if (comptime Environment.isDebug) {
bun.assertWithLocation(std.fs.path.isAbsolute(target), @src());
}
return .{ .root_dir = null, .target = target };
}
/// Create a `Preload` from a path relative to some absolute root directory path.
pub fn initRelative(root_dir: []const u8, target: []const u8) Preload {
if (comptime Environment.isDebug) {
bun.assertWithLocation(std.fs.path.isAbsolute(root_dir), @src());
}
return Preload{ .root_dir = root_dir, .target = target };
}
/// Create a `Preload` from a path relative to the current working directory.
pub fn initCwd(target: []const u8) Preload {
return Preload{ .root_dir = null, .target = target };
}
/// A preload that may be relative to some file declaring it.
/// `from_file` is a path to a file. It may be relative or absolute.
pub fn initRelativeToFile(from_file: []const u8, target: []const u8) Preload {
// Its safe to assume that file extensions are less than 16 characters.
// This lets us halt our search early, saving time on long paths.
const max_ext = 16;
const section_to_search = if (from_file.len > max_ext) from_file[from_file.len - max_ext ..] else from_file;
// in case they pass us a file with no extension or, accidentally, a directory.
// NOTE: lastIndexOfScalar uses SIMD on available targets.
const dirname = if (std.mem.lastIndexOfScalar(u8, section_to_search, '.')) |dot|
section_to_search[0..dot]
else
section_to_search;
return .{ .root_dir = dirname, .target = target };
}
};
pub fn create(allocator: std.mem.Allocator, log: *logger.Log, comptime command: Command.Tag) anyerror!Context {
Cli.cmd = command;
context_data = .{

View File

@@ -1109,7 +1109,7 @@ pub const TestCommand = struct {
},
);
vm.argv = ctx.passthrough;
vm.preload = ctx.preloads;
vm.preload = ctx.preloads.items;
vm.transpiler.options.rewrite_jest_for_tests = true;
vm.transpiler.options.env.behavior = .load_all_without_inlining;

View File

@@ -0,0 +1,2 @@
import assert from "node:assert";
assert.equal(globalThis.preload, "parent/preload.ts");

View File

@@ -112,22 +112,36 @@ describe("Given a `bunfig.toml` with a plugin preload", () => {
}); // </given a `bunfig.toml` with a plugin preload>
describe("Given a `bunfig.toml` file with a relative path to a preload in a parent directory", () => {
const dir = fixturePath("parent", "foo");
// FIXME
it("When `bun run` is run, preloads are run", async () => {
it("When `bun run` is run with a bunfig.toml, preloads are run", async () => {
const dir = fixturePath("parent", "foo");
const [out, err, code] = await run("index.ts", { cwd: dir });
expect(err).toBeEmpty();
expect(out).toBeEmpty();
expect(code).toBe(0);
});
it("When `bun run` is run with a bunfig.toml in a separate directory, preloads are run", async () => {
const dir = fixturePath("parent");
const [out, err, code] = await run("foo/index.ts", { args: ["--config=foo/bunfig.toml"], cwd: dir });
expect(err).toBeEmpty();
expect(out).toBeEmpty();
expect(code).toBe(0);
});
it("when `bun run` is run with --preload, preloads are run", async () => {
const dir = fixturePath("parent", "foo", "bar");
const [out, err, code] = await run("index.ts", { args: ["--preload=../../preload.ts"], cwd: dir });
expect(err).toBeEmpty();
expect(out).toBeEmpty();
expect(code).toBe(0);
});
}); // </given a `bunfit.toml` file with a relative path to a preload in a parent directory>
describe("Given a `bunfig.toml` file with a relative path without a leading './'", () => {
const dir = fixturePath("relative");
// FIXME: currently treaded as an import to an external package
it.skip("preload = 'preload.ts' is treated like a relative path and loaded", async () => {
it.todo("preload = 'preload.ts' is treated like a relative path and loaded", async () => {
const [out, err, code] = await run("index.ts", { cwd: dir });
expect(err).toBeEmpty();
expect(out).toBeEmpty();