aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/bun.js/api/JSBundler.zig31
-rw-r--r--src/bundler/bundle_v2.zig82
-rw-r--r--src/cli.zig10
-rw-r--r--src/cli/build_command.zig31
-rw-r--r--src/options.zig3
-rw-r--r--src/resolver/resolve_path.zig143
-rw-r--r--test/bundler/bundler_edgecase.test.ts12
-rw-r--r--test/bundler/bundler_naming.test.ts273
-rw-r--r--test/bundler/esbuild/default.test.ts8
-rw-r--r--test/bundler/esbuild/loader.test.ts30
-rw-r--r--test/bundler/expectBundled.ts27
11 files changed, 562 insertions, 88 deletions
diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig
index f17fe99d1..c95c359f1 100644
--- a/src/bun.js/api/JSBundler.zig
+++ b/src/bun.js/api/JSBundler.zig
@@ -11,6 +11,7 @@ const js = JSC.C;
const WebCore = @import("../webcore/response.zig");
const Bundler = bun.bundler;
const options = @import("../../options.zig");
+const resolve_path = @import("../../resolver/resolve_path.zig");
const VirtualMachine = JavaScript.VirtualMachine;
const ScriptSrcStream = std.io.FixedBufferStream([]u8);
const ZigString = JSC.ZigString;
@@ -54,6 +55,7 @@ pub const JSBundler = struct {
loaders: ?Api.LoaderMap = null,
dir: OwnedString = OwnedString.initEmpty(bun.default_allocator),
outdir: OwnedString = OwnedString.initEmpty(bun.default_allocator),
+ rootdir: OwnedString = OwnedString.initEmpty(bun.default_allocator),
serve: Serve = .{},
jsx: options.JSX.Pragma = .{},
code_splitting: bool = false,
@@ -74,6 +76,7 @@ pub const JSBundler = struct {
.define = bun.StringMap.init(allocator, true),
.dir = OwnedString.initEmpty(allocator),
.outdir = OwnedString.initEmpty(allocator),
+ .rootdir = OwnedString.initEmpty(allocator),
.names = .{
.owned_entry_point = OwnedString.initEmpty(allocator),
.owned_chunk = OwnedString.initEmpty(allocator),
@@ -253,6 +256,33 @@ pub const JSBundler = struct {
return error.JSException;
}
+ {
+ const path: ZigString.Slice = brk: {
+ if (try config.getOptional(globalThis, "root", ZigString.Slice)) |slice| {
+ break :brk slice;
+ }
+
+ const entry_points = this.entry_points.keys();
+
+ if (entry_points.len == 1) {
+ break :brk ZigString.Slice.fromUTF8NeverFree(std.fs.path.dirname(entry_points[0]) orelse ".");
+ }
+
+ break :brk ZigString.Slice.fromUTF8NeverFree(resolve_path.getIfExistsLongestCommonPath(entry_points) orelse ".");
+ };
+
+ defer path.deinit();
+
+ var dir = std.fs.cwd().openDir(path.slice(), .{}) catch |err| {
+ globalThis.throwPretty("{s}: failed to open root directory: {s}", .{ @errorName(err), path.slice() });
+ return error.JSException;
+ };
+ defer dir.close();
+
+ var rootdir_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+ this.rootdir.appendSliceExact(try bun.getFdPath(dir.fd, &rootdir_buf)) catch unreachable;
+ }
+
if (try config.getArray(globalThis, "external")) |externals| {
var iter = externals.arrayIterator(globalThis);
while (iter.next()) |entry_point| {
@@ -455,6 +485,7 @@ pub const JSBundler = struct {
}
self.names.deinit();
self.outdir.deinit();
+ self.rootdir.deinit();
self.public_path.deinit();
}
};
diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig
index 0ddb4476e..57adc904a 100644
--- a/src/bundler/bundle_v2.zig
+++ b/src/bundler/bundle_v2.zig
@@ -1599,6 +1599,7 @@ pub const BundleV2 = struct {
bundler.options.public_path = config.public_path.list.items;
bundler.options.output_dir = config.outdir.toOwnedSliceLeaky();
+ bundler.options.root_dir = config.rootdir.toOwnedSliceLeaky();
bundler.options.minify_syntax = config.minify.syntax;
bundler.options.minify_whitespace = config.minify.whitespace;
bundler.options.minify_identifiers = config.minify.identifiers;
@@ -3865,7 +3866,15 @@ const LinkerContext = struct {
const pathname = Fs.PathName.init(this.graph.entry_points.items(.output_path)[chunk.entry_point.entry_point_id].slice());
chunk.template.placeholder.name = pathname.base;
chunk.template.placeholder.ext = "js";
- chunk.template.placeholder.dir = pathname.dir;
+
+ var dir = std.fs.cwd().openDir(pathname.dir, .{}) catch |err| {
+ try this.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{s}: failed to open entry point directory: {s}", .{ @errorName(err), pathname.dir });
+ return error.FailedToOpenEntryPointDirectory;
+ };
+ defer dir.close();
+
+ var real_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+ chunk.template.placeholder.dir = try resolve_path.relativeAlloc(this.allocator, this.resolver.opts.root_dir, try bun.getFdPath(dir.fd, &real_path_buf));
} else {
chunk.template = PathTemplate.chunk;
if (this.resolver.opts.chunk_naming.len > 0)
@@ -8653,7 +8662,8 @@ const LinkerContext = struct {
// TODO: enforceNoCyclicChunkImports()
{
-
+ var path_names_map = bun.StringHashMap(void).init(c.allocator);
+ defer path_names_map.deinit();
// Compute the final hashes of each chunk. This can technically be done in
// parallel but it probably doesn't matter so much because we're not hashing
// that much data.
@@ -8661,7 +8671,13 @@ const LinkerContext = struct {
// TODO: non-isolated-hash
chunk.template.placeholder.hash = chunk.isolated_hash;
- chunk.final_rel_path = std.fmt.allocPrint(c.allocator, "{any}", .{chunk.template}) catch unreachable;
+ const rel_path = std.fmt.allocPrint(c.allocator, "{any}", .{chunk.template}) catch unreachable;
+ if ((try path_names_map.getOrPut(rel_path)).found_existing) {
+ try c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "Multiple files share the same output path: {s}", .{rel_path});
+ return error.DuplicateOutputPath;
+ }
+
+ chunk.final_rel_path = rel_path;
}
}
@@ -8962,43 +8978,6 @@ const LinkerContext = struct {
return err;
};
defer root_dir.close();
- const from_path: []const u8 = brk: {
- var all_paths = c.allocator.alloc(
- []const u8,
- chunks.len +
- @as(
- usize,
- @boolToInt(
- react_client_components_manifest.len > 0,
- ),
- ) +
- c.parse_graph.additional_output_files.items.len,
- ) catch unreachable;
- defer c.allocator.free(all_paths);
-
- var remaining_paths = all_paths;
-
- for (all_paths[0..chunks.len], chunks) |*dest, src| {
- dest.* = src.final_rel_path;
- }
- remaining_paths = remaining_paths[chunks.len..];
-
- if (react_client_components_manifest.len > 0) {
- remaining_paths[0] = components_manifest_path;
- remaining_paths = remaining_paths[1..];
- }
-
- for (remaining_paths, c.parse_graph.additional_output_files.items) |*dest, output_file| {
- dest.* = output_file.input.text;
- }
-
- remaining_paths = remaining_paths[c.parse_graph.additional_output_files.items.len..];
-
- std.debug.assert(remaining_paths.len == 0);
-
- break :brk resolve_path.longestCommonPath(all_paths);
- };
-
// Optimization: when writing to disk, we can re-use the memory
var max_heap_allocator: bun.MaxHeapAllocator = undefined;
defer max_heap_allocator.deinit();
@@ -9023,19 +9002,16 @@ const LinkerContext = struct {
defer max_heap_allocator.reset();
var rel_path = chunk.final_rel_path;
- if (rel_path.len > from_path.len) {
- rel_path = resolve_path.relative(from_path, rel_path);
- if (std.fs.path.dirname(rel_path)) |parent| {
- if (parent.len > root_path.len) {
- root_dir.dir.makePath(parent) catch |err| {
- c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{s} creating outdir {} while saving chunk {}", .{
- @errorName(err),
- bun.fmt.quote(parent),
- bun.fmt.quote(chunk.final_rel_path),
- }) catch unreachable;
- return err;
- };
- }
+ if (std.fs.path.dirname(rel_path)) |rel_parent| {
+ if (rel_parent.len > 0) {
+ root_dir.dir.makePath(rel_parent) catch |err| {
+ c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{s} creating outdir {} while saving chunk {}", .{
+ @errorName(err),
+ bun.fmt.quote(rel_parent),
+ bun.fmt.quote(chunk.final_rel_path),
+ }) catch unreachable;
+ return err;
+ };
}
}
diff --git a/src/cli.zig b/src/cli.zig
index ff4c847c6..ca8208aa2 100644
--- a/src/cli.zig
+++ b/src/cli.zig
@@ -196,6 +196,7 @@ pub const Arguments = struct {
clap.parseParam("--format <STR> Specifies the module format to build to. Only esm is supported.") catch unreachable,
clap.parseParam("--outdir <STR> Default to \"dist\" if multiple files") catch unreachable,
clap.parseParam("--outfile <STR> Write to a file") catch unreachable,
+ clap.parseParam("--root <STR> Root directory used for multiple entry points") catch unreachable,
clap.parseParam("--splitting Enable code splitting") catch unreachable,
// clap.parseParam("--manifest <STR> Write JSON manifest") catch unreachable,
// clap.parseParam("--public-path <STR> A prefix to be appended to any import paths in bundled code") catch unreachable,
@@ -489,6 +490,12 @@ pub const Arguments = struct {
}
}
+ if (args.option("--root")) |root_dir| {
+ if (root_dir.len > 0) {
+ ctx.bundler_options.root_dir = root_dir;
+ }
+ }
+
if (args.option("--format")) |format_str| {
const format = options.Format.fromString(format_str) orelse {
Output.prettyErrorln("<r><red>error<r>: Invalid format - must be esm, cjs, or iife", .{});
@@ -936,7 +943,8 @@ pub const Command = struct {
pub const BundlerOptions = struct {
outdir: []const u8 = "",
outfile: []const u8 = "",
- entry_naming: []const u8 = "./[name].[ext]",
+ root_dir: []const u8 = "",
+ entry_naming: []const u8 = "[dir]/[name].[ext]",
chunk_naming: []const u8 = "./[name]-[hash].[ext]",
asset_naming: []const u8 = "./[name]-[hash].[ext]",
react_server_components: bool = false,
diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig
index 5c9507a00..354c481cc 100644
--- a/src/cli/build_command.zig
+++ b/src/cli/build_command.zig
@@ -59,10 +59,10 @@ pub const BuildCommand = struct {
this_bundler.resolver.opts.entry_naming = ctx.bundler_options.entry_naming;
this_bundler.resolver.opts.chunk_naming = ctx.bundler_options.chunk_naming;
this_bundler.resolver.opts.asset_naming = ctx.bundler_options.asset_naming;
- this_bundler.options.output_dir = ctx.bundler_options.outdir;
- this_bundler.resolver.opts.output_dir = ctx.bundler_options.outdir;
+
this_bundler.options.react_server_components = ctx.bundler_options.react_server_components;
this_bundler.resolver.opts.react_server_components = ctx.bundler_options.react_server_components;
+
this_bundler.options.code_splitting = ctx.bundler_options.code_splitting;
this_bundler.resolver.opts.code_splitting = ctx.bundler_options.code_splitting;
@@ -84,6 +84,33 @@ pub const BuildCommand = struct {
this_bundler.options.output_dir = ctx.bundler_options.outdir;
this_bundler.resolver.opts.output_dir = ctx.bundler_options.outdir;
+ var src_root_dir_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+ const src_root_dir: string = brk1: {
+ const path = brk2: {
+ if (ctx.bundler_options.root_dir.len > 0) {
+ break :brk2 ctx.bundler_options.root_dir;
+ }
+
+ if (this_bundler.options.entry_points.len == 1) {
+ break :brk2 std.fs.path.dirname(this_bundler.options.entry_points[0]) orelse ".";
+ }
+
+ break :brk2 resolve_path.getIfExistsLongestCommonPath(this_bundler.options.entry_points) orelse ".";
+ };
+
+ var dir = std.fs.cwd().openDir(path, .{}) catch |err| {
+ Output.prettyErrorln("<r>error<r>: {s}: failed to open root directory: {s}", .{ @errorName(err), path });
+ Global.exit(1);
+ return;
+ };
+ defer dir.close();
+
+ break :brk1 try bun.getFdPath(dir.fd, &src_root_dir_buf);
+ };
+
+ this_bundler.options.root_dir = src_root_dir;
+ this_bundler.resolver.opts.root_dir = src_root_dir;
+
this_bundler.options.react_server_components = ctx.bundler_options.react_server_components;
this_bundler.resolver.opts.react_server_components = ctx.bundler_options.react_server_components;
this_bundler.options.code_splitting = ctx.bundler_options.code_splitting;
diff --git a/src/options.zig b/src/options.zig
index 0c0a7372a..f30594516 100644
--- a/src/options.zig
+++ b/src/options.zig
@@ -1390,6 +1390,7 @@ pub const BundleOptions = struct {
output_dir_handle: ?Dir = null,
output_dir: string = "out",
+ root_dir: string = "",
node_modules_bundle_url: string = "",
node_modules_bundle_pretty_path: string = "",
@@ -2776,7 +2777,7 @@ pub const PathTemplate = struct {
};
switch (field) {
- .dir => try writer.writeAll(self.placeholder.dir),
+ .dir => try writer.writeAll(if (self.placeholder.dir.len > 0) self.placeholder.dir else "."),
.name => try writer.writeAll(self.placeholder.name),
.ext => try writer.writeAll(self.placeholder.ext),
.hash => {
diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig
index da21f3ec1..c5ffdc626 100644
--- a/src/resolver/resolve_path.zig
+++ b/src/resolver/resolve_path.zig
@@ -36,6 +36,145 @@ inline fn @"is ../"(slice: []const u8) bool {
return strings.hasPrefixComptime(slice, "../");
}
+pub fn getIfExistsLongestCommonPathGeneric(input: []const []const u8, comptime separator: u8, comptime isPathSeparator: IsSeparatorFunc) ?[]const u8 {
+ var min_length: usize = std.math.maxInt(usize);
+ for (input) |str| {
+ min_length = @min(str.len, min_length);
+ }
+
+ var index: usize = 0;
+ var last_common_separator: ?usize = null;
+
+ // try to use an unrolled version of this loop
+ switch (input.len) {
+ 0 => {
+ return "";
+ },
+ 1 => {
+ return input[0];
+ },
+ 2 => {
+ while (index < min_length) : (index += 1) {
+ if (input[0][index] != input[1][index]) {
+ if (last_common_separator == null) return null;
+ break;
+ }
+ if (@call(.always_inline, isPathSeparator, .{input[0][index]})) {
+ last_common_separator = index;
+ }
+ }
+ },
+ 3 => {
+ while (index < min_length) : (index += 1) {
+ if (nqlAtIndex(3, index, input)) {
+ if (last_common_separator == null) return null;
+ break;
+ }
+ if (@call(.always_inline, isPathSeparator, .{input[0][index]})) {
+ last_common_separator = index;
+ }
+ }
+ },
+ 4 => {
+ while (index < min_length) : (index += 1) {
+ if (nqlAtIndex(4, index, input)) {
+ if (last_common_separator == null) return null;
+ break;
+ }
+ if (@call(.always_inline, isPathSeparator, .{input[0][index]})) {
+ last_common_separator = index;
+ }
+ }
+ },
+ 5 => {
+ while (index < min_length) : (index += 1) {
+ if (nqlAtIndex(5, index, input)) {
+ if (last_common_separator == null) return null;
+ break;
+ }
+ if (@call(.always_inline, isPathSeparator, .{input[0][index]})) {
+ last_common_separator = index;
+ }
+ }
+ },
+ 6 => {
+ while (index < min_length) : (index += 1) {
+ if (nqlAtIndex(6, index, input)) {
+ if (last_common_separator == null) return null;
+ break;
+ }
+ if (@call(.always_inline, isPathSeparator, .{input[0][index]})) {
+ last_common_separator = index;
+ }
+ }
+ },
+ 7 => {
+ while (index < min_length) : (index += 1) {
+ if (nqlAtIndex(7, index, input)) {
+ if (last_common_separator == null) return null;
+ break;
+ }
+ if (@call(.always_inline, isPathSeparator, .{input[0][index]})) {
+ last_common_separator = index;
+ }
+ }
+ },
+ 8 => {
+ while (index < min_length) : (index += 1) {
+ if (nqlAtIndex(8, index, input)) {
+ if (last_common_separator == null) return null;
+ break;
+ }
+ if (@call(.always_inline, isPathSeparator, .{input[0][index]})) {
+ last_common_separator = index;
+ }
+ }
+ },
+ else => {
+ var string_index: usize = 1;
+ while (string_index < input.len) : (string_index += 1) {
+ while (index < min_length) : (index += 1) {
+ if (input[0][index] != input[string_index][index]) {
+ if (last_common_separator == null) return null;
+ break;
+ }
+ }
+ if (index == min_length) index -= 1;
+ if (@call(.always_inline, isPathSeparator, .{input[0][index]})) {
+ last_common_separator = index;
+ }
+ }
+ },
+ }
+
+ if (index == 0) {
+ return &([_]u8{separator});
+ }
+
+ if (last_common_separator == null) {
+ return &([_]u8{'.'});
+ }
+
+ // The above won't work for a case like this:
+ // /app/public/index.js
+ // /app/public
+ // It will return:
+ // /app/
+ // It should return:
+ // /app/public/
+ // To detect /app/public is actually a folder, we do one more loop through the strings
+ // and say, "do one of you have a path separator after what we thought was the end?"
+ for (input) |str| {
+ if (str.len > index) {
+ if (@call(.always_inline, isPathSeparator, .{str[index]})) {
+ return str[0 .. index + 1];
+ }
+ }
+ }
+
+ return input[0][0 .. last_common_separator.? + 1];
+}
+
// TODO: is it faster to determine longest_common_separator in the while loop
// or as an extra step at the end?
// only boether to check if this function appears in benchmarking
@@ -170,6 +309,10 @@ pub fn longestCommonPath(input: []const []const u8) []const u8 {
return longestCommonPathGeneric(input, '/', isSepAny);
}
+pub fn getIfExistsLongestCommonPath(input: []const []const u8) ?[]const u8 {
+ return getIfExistsLongestCommonPathGeneric(input, '/', isSepAny);
+}
+
pub fn longestCommonPathWindows(input: []const []const u8) []const u8 {
return longestCommonPathGeneric(input, std.fs.path.sep_windows, isSepWin32);
}
diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts
index 5e19d091c..4960c7d39 100644
--- a/test/bundler/bundler_edgecase.test.ts
+++ b/test/bundler/bundler_edgecase.test.ts
@@ -632,6 +632,18 @@ describe("bundler", () => {
`,
},
});
+ itBundled("edgecase/AbsolutePathShouldNotResolveAsRelative", {
+ notImplemented: true,
+ files: {
+ "/entry.js": /* js */ `
+ console.log(1);
+ `,
+ },
+ entryPointsRaw: ["/entry.js"],
+ bundleErrors: {
+ "<bun>": ['ModuleNotFound resolving "/entry.js" (entry point)'],
+ },
+ });
itBundled("edgecase/ExportDefaultUndefined", {
files: {
"/entry.ts": /* ts */ `
diff --git a/test/bundler/bundler_naming.test.ts b/test/bundler/bundler_naming.test.ts
new file mode 100644
index 000000000..b358e064c
--- /dev/null
+++ b/test/bundler/bundler_naming.test.ts
@@ -0,0 +1,273 @@
+import assert from "assert";
+import dedent from "dedent";
+import { ESBUILD, itBundled, testForFile } from "./expectBundled";
+var { describe, test, expect } = testForFile(import.meta.path);
+
+describe("bundler", () => {
+ itBundled("naming/EntryNamingCollission", {
+ files: {
+ "/a/entry.js": /* js */ `
+ console.log(1);
+ `,
+ "/b/entry.js": /* js */ `
+ console.log(2);
+ `,
+ },
+ entryNaming: "[name].[ext]",
+ entryPointsRaw: ["./a/entry.js", "./b/entry.js"],
+ bundleErrors: {
+ "<bun>": [`Multiple files share the same output path: ./entry.js`],
+ },
+ });
+ itBundled("naming/ImplicitOutbase1", {
+ files: {
+ "/a/entry.js": /* js */ `
+ console.log(1);
+ `,
+ "/b/entry.js": /* js */ `
+ console.log(2);
+ `,
+ },
+ entryPointsRaw: ["./a/entry.js", "./b/entry.js"],
+ run: [
+ {
+ file: "/out/a/entry.js",
+ stdout: "1",
+ },
+ {
+ file: "/out/b/entry.js",
+ stdout: "2",
+ },
+ ],
+ });
+ itBundled("naming/ImplicitOutbase2", {
+ files: {
+ "/a/hello/entry.js": /* js */ `
+ import data from '../dependency'
+ console.log(data);
+ `,
+ "/a/dependency.js": /* js */ `
+ export default 1;
+ `,
+ "/a/hello/world/entry.js": /* js */ `
+ console.log(2);
+ `,
+ "/a/hello/world/a/a/a/a/a/a/a/entry.js": /* js */ `
+ console.log(3);
+ `,
+ },
+ entryPointsRaw: ["./a/hello/entry.js", "./a/hello/world/entry.js", "./a/hello/world/a/a/a/a/a/a/a/entry.js"],
+ run: [
+ {
+ file: "/out/entry.js",
+ stdout: "1",
+ },
+ {
+ file: "/out/world/entry.js",
+ stdout: "2",
+ },
+ {
+ file: "/out/world/a/a/a/a/a/a/a/entry.js",
+ stdout: "3",
+ },
+ ],
+ });
+ itBundled("naming/EntryNamingTemplate1", {
+ files: {
+ "/a/hello/entry.js": /* js */ `
+ import data from '../dependency'
+ console.log(data);
+ `,
+ "/a/dependency.js": /* js */ `
+ export default 1;
+ `,
+ "/a/hello/world/entry.js": /* js */ `
+ console.log(2);
+ `,
+ "/a/hello/world/a/a/a/a/a/a/a/entry.js": /* js */ `
+ console.log(3);
+ `,
+ },
+ entryNaming: "files/[dir]/file.[ext]",
+ entryPointsRaw: ["./a/hello/entry.js", "./a/hello/world/entry.js", "./a/hello/world/a/a/a/a/a/a/a/entry.js"],
+ run: [
+ {
+ file: "/out/files/file.js",
+ stdout: "1",
+ },
+ {
+ file: "/out/files/world/file.js",
+ stdout: "2",
+ },
+ {
+ file: "/out/files/world/a/a/a/a/a/a/a/file.js",
+ stdout: "3",
+ },
+ ],
+ });
+ itBundled("naming/EntryNamingTemplate2", {
+ notImplemented: true,
+ files: {
+ "/src/first.js": /* js */ `
+ console.log(1);
+ `,
+ "/src/second/third.js": /* js */ `
+ console.log(2);
+ `,
+ },
+ entryNaming: "[ext]/prefix[dir]suffix/file.[ext]",
+ entryPointsRaw: ["./src/first.js", "./src/second/third.js"],
+ run: [
+ {
+ file: "/out/js/prefix/secondsuffix/file.js",
+ stdout: "2",
+ },
+ {
+ file: "/out/js/prefix/suffix/file.js",
+ stdout: "1",
+ },
+ ],
+ });
+ itBundled("naming/AssetNaming", {
+ files: {
+ "/src/lib/first/file.js": /* js */ `
+ import file from "../second/data.file";
+ console.log(file);
+ `,
+ "/src/lib/second/data.file": `
+ this is a file
+ `,
+ },
+ root: "/src",
+ entryNaming: "hello.[ext]",
+ assetNaming: "test.[ext]",
+ entryPointsRaw: ["./src/lib/first/file.js"],
+ run: {
+ file: "/out/hello.js",
+ stdout: "./test.file",
+ },
+ });
+ itBundled("naming/AssetNamingMkdir", {
+ files: {
+ "/src/lib/first/file.js": /* js */ `
+ import file from "../second/data.file";
+ console.log(file);
+ `,
+ "/src/lib/second/data.file": `
+ this is a file
+ `,
+ },
+ root: "/src",
+ entryNaming: "hello.[ext]",
+ assetNaming: "subdir/test.[ext]",
+ entryPointsRaw: ["./src/lib/first/file.js"],
+ run: {
+ file: "/out/hello.js",
+ stdout: "./subdir/test.file",
+ },
+ });
+ itBundled("naming/AssetNamingDir", {
+ notImplemented: true,
+ files: {
+ "/src/lib/first/file.js": /* js */ `
+ import file from "../second/data.file";
+ console.log(file);
+ `,
+ "/src/lib/second/data.file": `
+ this is a file
+ `,
+ },
+ root: "/src",
+ entryNaming: "hello.[ext]",
+ assetNaming: "[dir]/test.[ext]",
+ entryPointsRaw: ["./src/lib/first/file.js"],
+ loader: ESBUILD
+ ? {
+ ".file": "file",
+ }
+ : undefined,
+ run: [
+ {
+ file: "/out/hello.js",
+ stdout: "./lib/second/test.file",
+ },
+ ],
+ });
+ itBundled("naming/AssetNoOverwrite", {
+ notImplemented: true,
+ files: {
+ "/src/entry.js": /* js */ `
+ import asset1 from "./asset1.file";
+ import asset2 from "./asset2.file";
+ console.log(asset1, asset2);
+ `,
+ "/src/asset1.file": `
+ file 1
+ `,
+ "/src/asset2.file": `
+ file 2
+ `,
+ },
+ root: "/src",
+ assetNaming: "same-filename.txt",
+ entryPointsRaw: ["./src/entry.js"],
+ loader: {
+ ".file": "file",
+ },
+ bundleErrors: {
+ "<bun>": ['Multiple files share the same output path: "same-filename.txt"'],
+ },
+ });
+ itBundled("naming/AssetFileLoaderPath1", {
+ files: {
+ "/src/entry.js": /* js */ `
+ import asset1 from "./asset1.file";
+ console.log(asset1);
+ `,
+ "/src/asset1.file": `
+ file 1
+ `,
+ //
+ "/out/hello/_": "",
+ },
+ root: "/src",
+ entryNaming: "lib/entry.js",
+ assetNaming: "hello/same-filename.txt",
+ entryPointsRaw: ["./src/entry.js"],
+ loader: {
+ ".file": "file",
+ },
+ });
+ itBundled("naming/NonexistantRoot", ({ root }) => ({
+ files: {
+ "/src/entry.js": /* js */ `
+ import asset1 from "./asset1.file";
+ console.log(asset1);
+ `,
+ "/src/asset1.file": `
+ file 1
+ `,
+ },
+ root: "/lib",
+ entryPointsRaw: ["./src/entry.js"],
+ bundleErrors: {
+ "<bun>": [`FileNotFound: failed to open root directory: ${root}/lib`],
+ },
+ }));
+ itBundled("naming/EntrypointOutsideOfRoot", {
+ notImplemented: true,
+ files: {
+ "/src/hello/entry.js": /* js */ `
+ console.log(1);
+ `,
+ "/src/root/file.js": /* js */ `
+ console.log(2);
+ `,
+ },
+ root: "/src/root",
+ entryPointsRaw: ["./src/hello/entry.js"],
+ run: {
+ file: "/out/_.._/hello/file.js",
+ },
+ });
+});
diff --git a/test/bundler/esbuild/default.test.ts b/test/bundler/esbuild/default.test.ts
index 39cb293be..c668ee667 100644
--- a/test/bundler/esbuild/default.test.ts
+++ b/test/bundler/esbuild/default.test.ts
@@ -4043,8 +4043,8 @@ describe("bundler", () => {
"/a/b/c.js": `console.log('c')`,
"/a/b/d.js": `console.log('d')`,
},
- entryPoints: ["/a/b/c.js", "/a/b/d.js"],
- outbase: "/",
+ entryPointsRaw: ["/a/b/c.js", "/a/b/d.js"],
+ root: "/",
onAfterBundle(api) {
api.assertFileExists("/out/a/b/c.js");
api.assertFileExists("/out/a/b/d.js");
@@ -4930,7 +4930,7 @@ describe("bundler", () => {
splitting: true,
outdir: "/out",
format: "esm",
- outbase: "/some/nested/directory",
+ root: "/some/nested/directory",
});
const relocateFiles = {
"/top-level.js": /* js */ `
@@ -5279,7 +5279,7 @@ describe("bundler", () => {
// "/src/lib/shared.js": `console.log('shared')`,
// },
// entryPoints: ["/src/entries/entry1.js", "/src/entries/entry2.js"],
- // outbase: "/src",
+ // root: "/src",
// splitting: true,
// entryNaming: "main/[ext]/[name]-[hash].[ext]",
// });
diff --git a/test/bundler/esbuild/loader.test.ts b/test/bundler/esbuild/loader.test.ts
index a2376a3ce..97e26eb61 100644
--- a/test/bundler/esbuild/loader.test.ts
+++ b/test/bundler/esbuild/loader.test.ts
@@ -305,7 +305,7 @@ describe("bundler", () => {
`,
"/src/images/image.png": `x`,
},
- outbase: "/src",
+ root: "/src",
outdir: "/out",
outputPaths: ["/out/entries/entry.js"],
loader: {
@@ -337,7 +337,7 @@ describe("bundler", () => {
`,
"/src/images/image.png": `x`,
},
- outbase: "/src",
+ root: "/src",
assetNaming: "[dir]/[name]-[hash]",
outdir: "/out",
outputPaths: ["/out/entries/entry.js"],
@@ -359,7 +359,7 @@ describe("bundler", () => {
"/src/images/image.png": `x`,
"/src/uploads/file.txt": `y`,
},
- outbase: "/src",
+ root: "/src",
assetNaming: "[ext]/[name]-[hash]",
});
itBundled("loader/FileRelativePathAssetNamesCSS", {
@@ -372,7 +372,7 @@ describe("bundler", () => {
`,
"/src/images/image.png": `x`,
},
- outbase: "/src",
+ root: "/src",
assetNaming: "[dir]/[name]-[hash]",
});
itBundled("loader/FilePublicPathJS", {
@@ -384,7 +384,7 @@ describe("bundler", () => {
`,
"/src/images/image.png": `x`,
},
- outbase: "/src",
+ root: "/src",
publicPath: "https://example.com",
});
itBundled("loader/FilePublicPathCSS", {
@@ -397,7 +397,7 @@ describe("bundler", () => {
`,
"/src/images/image.png": `x`,
},
- outbase: "/src",
+ root: "/src",
publicPath: "https://example.com",
});
itBundled("loader/FilePublicPathAssetNamesJS", {
@@ -409,7 +409,7 @@ describe("bundler", () => {
`,
"/src/images/image.png": `x`,
},
- outbase: "/src",
+ root: "/src",
publicPath: "https://example.com",
assetNaming: "[dir]/[name]-[hash]",
});
@@ -423,7 +423,7 @@ describe("bundler", () => {
`,
"/src/images/image.png": `x`,
},
- outbase: "/src",
+ root: "/src",
publicPath: "https://example.com",
assetNaming: "[dir]/[name]-[hash]",
});
@@ -439,7 +439,7 @@ describe("bundler", () => {
"/src/shared/common.png": `x`,
},
entryPoints: ["/src/entries/entry.js", "/src/entries/other/entry.js"],
- outbase: "/src",
+ root: "/src",
});
itBundled("loader/FileOneSourceTwoDifferentOutputPathsCSS", {
// GENERATED
@@ -454,7 +454,7 @@ describe("bundler", () => {
"/src/shared/common.png": `x`,
},
entryPoints: ["/src/entries/entry.css", "/src/entries/other/entry.css"],
- outbase: "/src",
+ root: "/src",
});
itBundled("loader/JSONNoBundle", {
// GENERATED
@@ -691,7 +691,7 @@ describe("bundler", () => {
`,
"/Users/user/project/assets/some.file": `stuff`,
},
- outbase: "/Users/user/project",
+ root: "/Users/user/project",
});
itBundled("loader/CopyWithBundleFromCSS", {
// GENERATED
@@ -703,7 +703,7 @@ describe("bundler", () => {
`,
"/Users/user/project/assets/some.file": `stuff`,
},
- outbase: "/Users/user/project",
+ root: "/Users/user/project",
});
itBundled("loader/CopyWithBundleEntryPoint", {
// GENERATED
@@ -724,7 +724,7 @@ describe("bundler", () => {
"/Users/user/project/src/entry.css",
"/Users/user/project/assets/some.file",
],
- outbase: "/Users/user/project",
+ root: "/Users/user/project",
});
itBundled("loader/CopyWithTransform", {
// GENERATED
@@ -733,7 +733,7 @@ describe("bundler", () => {
"/Users/user/project/assets/some.file": `stuff`,
},
entryPoints: ["/Users/user/project/src/entry.js", "/Users/user/project/assets/some.file"],
- outbase: "/Users/user/project",
+ root: "/Users/user/project",
mode: "passthrough",
});
itBundled("loader/CopyWithFormat", {
@@ -744,7 +744,7 @@ describe("bundler", () => {
},
entryPoints: ["/Users/user/project/src/entry.js", "/Users/user/project/assets/some.file"],
format: "iife",
- outbase: "/Users/user/project",
+ root: "/Users/user/project",
mode: "convertformat",
});
itBundled("loader/JSXAutomaticNoNameCollision", {
diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts
index ff9672237..471cdd7f2 100644
--- a/test/bundler/expectBundled.ts
+++ b/test/bundler/expectBundled.ts
@@ -133,7 +133,7 @@ export interface BundlerTestInput {
factory?: string; // for classic
fragment?: string; // for classic
};
- outbase?: string;
+ root?: string;
/** Defaults to `/out.js` */
outfile?: string;
/** Defaults to `/out` */
@@ -340,7 +340,7 @@ function expectBundled(
minifyWhitespace,
notImplemented,
onAfterBundle,
- outbase,
+ root: outbase,
outdir,
outfile,
outputPaths,
@@ -405,9 +405,6 @@ function expectBundled(
if (!ESBUILD && unsupportedCSSFeatures && unsupportedCSSFeatures.length) {
throw new Error("unsupportedCSSFeatures not implemented in bun build");
}
- if (!ESBUILD && outbase) {
- throw new Error("outbase/root not implemented in bun build");
- }
if (!ESBUILD && keepNames) {
throw new Error("keepNames not implemented in bun build");
}
@@ -463,10 +460,14 @@ function expectBundled(
}
if (outdir) {
- entryNaming ??= "[name].[ext]";
+ entryNaming ??= "[dir]/[name].[ext]";
chunkNaming ??= "[name]-[hash].[ext]";
}
+ if (outbase) {
+ outbase = path.join(root, outbase);
+ }
+
// Option validation
if (entryPaths.length !== 1 && outfile && !entryPointsRaw) {
throw new Error("Test cannot specify `outfile` when more than one entry path.");
@@ -536,12 +537,12 @@ function expectBundled(
jsx.importSource && ["--jsx-import-source", jsx.importSource],
// metafile && `--manifest=${metafile}`,
sourceMap && `--sourcemap=${sourceMap}`,
- entryNaming && entryNaming !== "[name].[ext]" && [`--entry-naming`, entryNaming],
+ entryNaming && entryNaming !== "[dir]/[name].[ext]" && [`--entry-naming`, entryNaming],
chunkNaming && chunkNaming !== "[name]-[hash].[ext]" && [`--chunk-naming`, chunkNaming],
- assetNaming && assetNaming !== "[name]-[hash].[ext]" && [`--asset-naming`, chunkNaming],
+ assetNaming && assetNaming !== "[name]-[hash].[ext]" && [`--asset-naming`, assetNaming],
splitting && `--splitting`,
serverComponents && "--server-components",
- outbase && `--outbase=${outbase}`,
+ outbase && `--root=${outbase}`,
// inject && inject.map(x => ["--inject", path.join(root, x)]),
// jsx.preserve && "--jsx=preserve",
// legalComments && `--legal-comments=${legalComments}`,
@@ -569,7 +570,9 @@ function expectBundled(
jsx.factory && `--jsx-factory=${jsx.factory}`,
jsx.fragment && `--jsx-fragment=${jsx.fragment}`,
env?.NODE_ENV !== "production" && `--jsx-dev`,
- entryNaming && entryNaming !== "[name].[ext]" && `--entry-names=${entryNaming.replace(/\.\[ext]$/, "")}`,
+ entryNaming &&
+ entryNaming !== "[dir]/[name].[ext]" &&
+ `--entry-names=${entryNaming.replace(/\.\[ext]$/, "")}`,
chunkNaming &&
chunkNaming !== "[name]-[hash].[ext]" &&
`--chunk-names=${chunkNaming.replace(/\.\[ext]$/, "")}`,
@@ -582,7 +585,7 @@ function expectBundled(
legalComments && `--legal-comments=${legalComments}`,
splitting && `--splitting`,
treeShaking && `--tree-shaking`,
- outbase && `--outbase=${path.join(root, outbase)}`,
+ outbase && `--outbase=${outbase}`,
keepNames && `--keep-names`,
mainFields && `--main-fields=${mainFields.join(",")}`,
loader && Object.entries(loader).map(([k, v]) => `--loader:${k}=${v}`),
@@ -1031,7 +1034,7 @@ for (const [key, blob] of build.outputs) {
}
} else {
// entryNames makes it so we cannot predict the output file
- if (!entryNaming || entryNaming === "[name].[ext]") {
+ if (!entryNaming || entryNaming === "[dir]/[name].[ext]") {
for (const fullpath of outputPaths) {
if (!existsSync(fullpath)) {
throw new Error("Bundle was not written to disk: " + fullpath);