diff options
-rw-r--r-- | src/bun.js/api/JSBundler.zig | 31 | ||||
-rw-r--r-- | src/bundler/bundle_v2.zig | 82 | ||||
-rw-r--r-- | src/cli.zig | 10 | ||||
-rw-r--r-- | src/cli/build_command.zig | 31 | ||||
-rw-r--r-- | src/options.zig | 3 | ||||
-rw-r--r-- | src/resolver/resolve_path.zig | 143 | ||||
-rw-r--r-- | test/bundler/bundler_edgecase.test.ts | 12 | ||||
-rw-r--r-- | test/bundler/bundler_naming.test.ts | 273 | ||||
-rw-r--r-- | test/bundler/esbuild/default.test.ts | 8 | ||||
-rw-r--r-- | test/bundler/esbuild/loader.test.ts | 30 | ||||
-rw-r--r-- | test/bundler/expectBundled.ts | 27 |
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); |