diff --git a/src/api/schema.zig b/src/api/schema.zig index 69604d2be1d52..78531473920e9 100644 --- a/src/api/schema.zig +++ b/src/api/schema.zig @@ -2879,6 +2879,9 @@ pub const Api = struct { /// concurrent_scripts concurrent_scripts: ?u32 = null, + /// ignore_workspace + ignore_workspace: ?bool = null, + pub fn decode(reader: anytype) anyerror!BunInstall { var this = std.mem.zeroes(BunInstall); diff --git a/src/bunfig.zig b/src/bunfig.zig index 6b4167fcfbfd3..eb2f853fc9e5e 100644 --- a/src/bunfig.zig +++ b/src/bunfig.zig @@ -506,6 +506,12 @@ pub const Bunfig = struct { } } } + + if (_bun.get("ignoreWorkspace")) |optional| { + if (optional.asBool()) |ignore_workspace| { + install.ignore_workspace = ignore_workspace; + } + } } if (json.get("run")) |run_expr| { diff --git a/src/cli.zig b/src/cli.zig index 6cbd5086ad7be..daab71696342d 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -266,6 +266,7 @@ pub const Arguments = struct { }; defer config_file.close(); + const contents = config_file.readToEndAlloc(allocator, std.math.maxInt(usize)) catch |err| { if (auto_loaded) return; Output.prettyErrorln("error: {s} reading config \"{s}\"", .{ @@ -286,6 +287,7 @@ pub const Arguments = struct { ctx.log.level = original_level; } ctx.log.level = logger.Log.Level.warn; + try Bunfig.parse(allocator, logger.Source.initPathString(bun.asByteSlice(config_path), contents), ctx, cmd); } @@ -297,7 +299,18 @@ pub const Arguments = struct { return null; } + pub fn loadConfig(allocator: std.mem.Allocator, user_config_path_: ?string, ctx: Command.Context, comptime cmd: Command.Tag) !void { + return loadConfig_(allocator, user_config_path_, ctx, cmd, false); + } + + pub fn loadConfig_( + allocator: std.mem.Allocator, + user_config_path_: ?string, + ctx: Command.Context, + comptime cmd: Command.Tag, + override_absolute_working_dir: ?bool, + ) !void { var config_buf: [bun.MAX_PATH_BYTES]u8 = undefined; if (comptime cmd.readGlobalConfig()) { if (!ctx.has_loaded_global_config) { @@ -310,7 +323,6 @@ pub const Arguments = struct { } var config_path_: []const u8 = user_config_path_ orelse ""; - var auto_loaded: bool = false; if (config_path_.len == 0 and (user_config_path_ != null or Command.Tag.always_loads_config.get(cmd) or @@ -334,7 +346,7 @@ pub const Arguments = struct { config_buf[config_path_.len] = 0; config_path = config_buf[0..config_path_.len :0]; } else { - if (ctx.args.absolute_working_dir == null) { + if ((override_absolute_working_dir orelse false) or ctx.args.absolute_working_dir == null) { var secondbuf: [bun.MAX_PATH_BYTES]u8 = undefined; const cwd = bun.getcwd(&secondbuf) catch return; @@ -348,6 +360,7 @@ pub const Arguments = struct { &parts, .auto, ); + config_buf[config_path_.len] = 0; config_path = config_buf[0..config_path_.len :0]; } @@ -1447,6 +1460,7 @@ pub const Command = struct { }, .InstallCommand => { if (comptime bun.fast_debug_build_mode and bun.fast_debug_build_cmd != .InstallCommand) unreachable; + const ctx = try Command.init(allocator, log, .InstallCommand); try InstallCommand.exec(ctx); @@ -2224,7 +2238,7 @@ pub const Command = struct { Output.flush(); }, Command.Tag.HelpCommand => { - HelpCommand.printWithReason(.explicit); + HelpCommand.printWithReason(.explicit, show_all_flags); }, Command.Tag.UpgradeCommand => { const intro_text = @@ -2283,7 +2297,7 @@ pub const Command = struct { Output.flush(); }, else => { - HelpCommand.printWithReason(.explicit); + HelpCommand.printWithReason(.explicit, show_all_flags); }, } } @@ -2329,6 +2343,7 @@ pub const Command = struct { pub const uses_global_options: std.EnumArray(Tag, bool) = std.EnumArray(Tag, bool).initDefault(true, .{ .CreateCommand = false, + .BunxCommand = false, .InstallCommand = false, .AddCommand = false, .RemoveCommand = false, @@ -2336,7 +2351,6 @@ pub const Command = struct { .PackageManagerCommand = false, .LinkCommand = false, .UnlinkCommand = false, - .BunxCommand = false, }); }; }; diff --git a/src/install/install.zig b/src/install/install.zig index b557df8befd3b..78d45e1bb833b 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -6978,6 +6978,7 @@ pub const PackageManager = struct { pub fn init(ctx: Command.Context, comptime subcommand: Subcommand) !*PackageManager { const cli = try CommandLineArguments.parse(ctx.allocator, subcommand); + return initWithCLI(ctx, cli, subcommand); } @@ -7010,6 +7011,8 @@ pub const PackageManager = struct { var workspace_names = Package.WorkspaceMap.init(ctx.allocator); + const before_top_level_dir = try ctx.allocator.dupe(u8, fs.top_level_dir); + // Step 1. Find the nearest package.json directory // // We will walk up from the cwd, trying to find the nearest package.json file. @@ -7082,9 +7085,24 @@ pub const PackageManager = struct { }; const child_cwd = this_cwd; + + // Reading a first time the config to know if we need to ignore the workspace or not + try BunArguments.loadConfig(ctx.allocator, cli.config, ctx, .InstallCommand); + + var ignore_workspace = false; + + if (cli.ignore_workspace) { + ignore_workspace = true; + } + + if (ctx.install) |install_| { + ignore_workspace = install_.ignore_workspace orelse false; + } + // Check if this is a workspace; if so, use root package var found = false; - if (comptime subcommand != .link) { + + if ((comptime subcommand != .link) and !ignore_workspace) { if (!created_package_json) { while (std.fs.path.dirname(this_cwd)) |parent| : (this_cwd = parent) { const parent_without_trailing_slash = strings.withoutTrailingSlash(parent); @@ -7152,8 +7170,15 @@ pub const PackageManager = struct { break :brk child_json; }; - try bun.sys.chdir(fs.top_level_dir).unwrap(); - try BunArguments.loadConfig(ctx.allocator, cli.config, ctx, .InstallCommand); + // If the top level dir is different than before, then we don't ignore the workspace and we found one, + // so we need to read again the config and override the working dir + if (!strings.eql(before_top_level_dir, fs.top_level_dir)) { + try bun.sys.chdir(fs.top_level_dir).unwrap(); + ctx.args.absolute_working_dir = try ctx.allocator.dupe(u8, fs.top_level_dir); + + try BunArguments.loadConfig_(ctx.allocator, cli.config, ctx, .InstallCommand, true); + } + bun.copy(u8, &cwd_buf, fs.top_level_dir); cwd_buf[fs.top_level_dir.len] = '/'; cwd_buf[fs.top_level_dir.len + 1] = 0; @@ -7278,6 +7303,7 @@ pub const PackageManager = struct { break :brk @as(u32, @truncate(@as(u64, @intCast(@max(std.time.timestamp(), 0))))); }; + return manager; } @@ -7773,6 +7799,7 @@ pub const PackageManager = struct { clap.parseParam("--no-progress Disable the progress bar") catch unreachable, clap.parseParam("--no-summary Don't print a summary") catch unreachable, clap.parseParam("--no-verify Skip verifying integrity of newly downloaded packages") catch unreachable, + clap.parseParam("--ignore-workspace Skip verifying if the current package is in a workspace or not") catch unreachable, clap.parseParam("--ignore-scripts Skip lifecycle scripts in the project's package.json (dependency scripts are never run)") catch unreachable, clap.parseParam("--trust Add to trustedDependencies in the project's package.json and install the package(s)") catch unreachable, clap.parseParam("-g, --global Install globally") catch unreachable, @@ -7849,6 +7876,7 @@ pub const PackageManager = struct { ignore_scripts: bool = false, trusted: bool = false, no_summary: bool = false, + ignore_workspace: bool = false, link_native_bins: []const string = &[_]string{}, @@ -8070,6 +8098,7 @@ pub const PackageManager = struct { cli.ignore_scripts = args.flag("--ignore-scripts"); cli.trusted = args.flag("--trust"); cli.no_summary = args.flag("--no-summary"); + cli.ignore_workspace = args.flag("--ignore-workspace"); // link and unlink default to not saving, all others default to // saving. @@ -8124,6 +8153,7 @@ pub const PackageManager = struct { buf[cwd_.len] = 0; final_path = buf[0..cwd_.len :0]; } + try bun.sys.chdir(final_path).unwrap(); } @@ -8988,7 +9018,7 @@ pub const PackageManager = struct { // pub fn printTreeDeps(this: *PackageInstaller) void { // for (this.tree_ids_to_trees_the_id_depends_on, 0..) |deps, j| { - // std.debug.print("tree #{d:3}: ", .{j}); + // std.debug.print("tree #{d:3}: ", .{j}); // for (0..this.lockfile.buffers.trees.items.len) |tree_id| { // std.debug.print("{d} ", .{@intFromBool(deps.isSet(tree_id))}); // } @@ -10674,9 +10704,11 @@ pub const PackageManager = struct { _ = manager.getCacheDirectory(); _ = manager.getTemporaryDirectory(); } + manager.enqueueDependencyList(root.dependencies); } else { // Anything that needs to be downloaded from an update needs to be scheduled here + manager.drainDependencyList(); } diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index bbdb8afa5ac72..3baa0318db2d8 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -1005,6 +1005,72 @@ it("should ignore peerDependencies within workspaces", async () => { await access(join(package_dir, "bun.lockb")); }); +it("should ignore workspaces when installing a package inside a workspace", async () => { + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + workspaces: ["bar", "packages/*"], + }), + ); + + const barPath = join(package_dir, "bar"); + await mkdir(barPath); + await writeFile( + join(package_dir, "bar", "package.json"), + JSON.stringify({ + name: "Bar", + version: "0.0.2", + }), + ); + + await mkdir(join(package_dir, "packages", "nominally-scoped"), { recursive: true }); + await writeFile( + join(package_dir, "packages", "nominally-scoped", "package.json"), + JSON.stringify({ + name: "@org/nominally-scoped", + version: "0.1.4", + }), + ); + + await mkdir(join(package_dir, "packages", "second-asterisk"), { recursive: true }); + await writeFile( + join(package_dir, "packages", "second-asterisk", "package.json"), + JSON.stringify({ + name: "AsteriskTheSecond", + version: "0.1.4", + }), + ); + + await mkdir(join(package_dir, "packages", "asterisk"), { recursive: true }); + await writeFile( + join(package_dir, "packages", "asterisk", "package.json"), + JSON.stringify({ + name: "Asterisk", + version: "0.0.4", + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--ignore-workspace"], + cwd: barPath, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).not.toContain("Saved lockfile"); + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*/, "").split(/\r?\n/)).toEqual(["done", ""]); + expect(await exited).toBe(0); + expect(requested).toBe(0); + expect(async () => await readdirSorted(join(package_dir, "node_modules"))).toThrow("No such file or directory"); +}); + it("should handle installing the same peerDependency with different versions", async () => { const urls: string[] = []; setHandler(dummyRegistry(urls)); @@ -6017,6 +6083,7 @@ it("should install dependencies in root package of workspace", async () => { workspaces: ["moo"], }), ); + await mkdir(join(package_dir, "moo")); const moo_package = JSON.stringify({ name: "moo", @@ -6039,6 +6106,7 @@ it("should install dependencies in root package of workspace", async () => { expect(err).toContain("Saved lockfile"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + moo@workspace:moo", @@ -6059,6 +6127,79 @@ it("should install dependencies in root package of workspace", async () => { await access(join(package_dir, "bun.lockb")); }); +it("should ignore workspaces when installing a package inside a workspace with bunfig", async () => { + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + workspaces: ["bar", "packages/*"], + }), + ); + + const barPath = join(package_dir, "bar"); + await mkdir(barPath); + await writeFile( + join(package_dir, "bar", "package.json"), + JSON.stringify({ + name: "Bar", + version: "0.0.2", + }), + ); + + await mkdir(join(package_dir, "packages", "nominally-scoped"), { recursive: true }); + await writeFile( + join(package_dir, "packages", "nominally-scoped", "package.json"), + JSON.stringify({ + name: "@org/nominally-scoped", + version: "0.1.4", + }), + ); + + await mkdir(join(package_dir, "packages", "second-asterisk"), { recursive: true }); + await writeFile( + join(package_dir, "packages", "second-asterisk", "package.json"), + JSON.stringify({ + name: "AsteriskTheSecond", + version: "0.1.4", + }), + ); + + await mkdir(join(package_dir, "packages", "asterisk"), { recursive: true }); + await writeFile( + join(package_dir, "packages", "asterisk", "package.json"), + JSON.stringify({ + name: "Asterisk", + version: "0.0.4", + }), + ); + + await writeFile( + join(package_dir, "bar", "bunfig.toml"), + `${await file(join(package_dir, "bunfig.toml")).text()} +ignoreWorkspace=true +`, + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: barPath, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).not.toContain("Saved lockfile"); + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*/, "").split(/\r?\n/)).toEqual(["done", ""]); + expect(await exited).toBe(0); + expect(requested).toBe(0); + expect(async () => await readdirSorted(join(package_dir, "node_modules"))).toThrow("No such file or directory"); +}); + it("should install dependencies in root package of workspace (*)", async () => { const urls: string[] = []; setHandler(dummyRegistry(urls)); @@ -7073,8 +7214,6 @@ it("should handle installing workspaces with multiple glob patterns", async () = }, }); - console.log("TEMPDIR", package_dir); - const { stdout, stderr } = await Bun.$`${bunExe()} install`.env(env).cwd(package_dir).throws(true); const err1 = stderr.toString(); expect(err1).toContain("Saved lockfile"); @@ -7143,7 +7282,6 @@ it.todo("should handle installing workspaces with absolute glob patterns", async }, }, }); - console.log("TEMP DIR", package_dir); const { stdout, stderr } = await Bun.$`${bunExe()} install`.env(env).cwd(package_dir).throws(true); const err1 = stderr.toString();