diff options
Diffstat (limited to 'src')
-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 |
6 files changed, 243 insertions, 57 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); } |