diff --git a/src/bun.js/api/glob.zig b/src/bun.js/api/glob.zig index edf341ec6a..3ace3a87de 100644 --- a/src/bun.js/api/glob.zig +++ b/src/bun.js/api/glob.zig @@ -406,7 +406,7 @@ pub fn match(this: *Glob, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame var str = str_arg.toSlice(globalThis, arena.allocator()); defer str.deinit(); - if (this.is_ascii and isAllAscii(str.slice())) return JSC.JSValue.jsBoolean(globImpl.Ascii.match(this.pattern, str.slice())); + if (this.is_ascii and isAllAscii(str.slice())) return JSC.JSValue.jsBoolean(globImpl.Ascii.match(this.pattern, str.slice()).matches()); const codepoints = codepoints: { if (this.pattern_codepoints) |cp| break :codepoints cp.items[0..]; diff --git a/src/cli/outdated_command.zig b/src/cli/outdated_command.zig index caa67f7b38..af827fcbcb 100644 --- a/src/cli/outdated_command.zig +++ b/src/cli/outdated_command.zig @@ -18,6 +18,8 @@ const FileSystem = bun.fs.FileSystem; const path = bun.path; const glob = bun.glob; const Table = bun.fmt.Table; +const WorkspaceFilter = PackageManager.WorkspaceFilter; +const OOM = bun.OOM; pub const OutdatedCommand = struct { pub fn exec(ctx: Command.Context) !void { @@ -138,7 +140,7 @@ pub const OutdatedCommand = struct { original_cwd: string, manager: *PackageManager, filters: []const string, - ) error{OutOfMemory}![]const PackageID { + ) OOM![]const PackageID { const lockfile = manager.lockfile; const packages = lockfile.packages.slice(); const pkg_names = packages.items(.name); @@ -152,36 +154,10 @@ pub const OutdatedCommand = struct { } const converted_filters = converted_filters: { - const buf = try allocator.alloc(FilterType, filters.len); + const buf = try allocator.alloc(WorkspaceFilter, filters.len); + var path_buf: bun.PathBuffer = undefined; for (filters, buf) |filter, *converted| { - if ((filter.len == 1 and filter[0] == '*') or strings.eqlComptime(filter, "**")) { - converted.* = .all; - continue; - } - - const is_path = filter.len > 0 and filter[0] == '.'; - - const joined_filter = if (is_path) - strings.withoutTrailingSlash(path.joinAbsString(original_cwd, &[_]string{filter}, .posix)) - else - filter; - - if (joined_filter.len == 0) { - converted.* = FilterType.init(&.{}, is_path); - continue; - } - - const length = bun.simdutf.length.utf32.from.utf8.le(joined_filter); - const convert_buf = try allocator.alloc(u32, length); - - const convert_result = bun.simdutf.convert.utf8.to.utf32.with_errors.le(joined_filter, convert_buf); - if (!convert_result.isSuccessful()) { - // nothing would match - converted.* = FilterType.init(&.{}, false); - continue; - } - - converted.* = FilterType.init(convert_buf[0..convert_result.count], is_path); + converted.* = try WorkspaceFilter.init(allocator, filter, original_cwd, &path_buf); } break :converted_filters buf; }; diff --git a/src/glob/GlobWalker.zig b/src/glob/GlobWalker.zig index f41b47f7a6..6498fbb7d4 100644 --- a/src/glob/GlobWalker.zig +++ b/src/glob/GlobWalker.zig @@ -1358,7 +1358,7 @@ pub fn GlobWalker_( return GlobAscii.match( pattern_component.patternSlice(this.pattern), filepath, - ); + ).matches(); } const codepoints = this.componentStringUnicode(pattern_component); return matchImpl( diff --git a/src/glob/ascii.zig b/src/glob/ascii.zig index 69413f9505..c2e4724cb1 100644 --- a/src/glob/ascii.zig +++ b/src/glob/ascii.zig @@ -181,6 +181,18 @@ pub fn valid_glob_indices(glob: []const u8, indices: std.ArrayList(BraceIndex)) } } +pub const MatchResult = enum { + no_match, + match, + + negate_no_match, + negate_match, + + pub fn matches(this: MatchResult) bool { + return this == .match or this == .negate_match; + } +}; + /// This function checks returns a boolean value if the pathname `path` matches /// the pattern `glob`. /// @@ -208,7 +220,7 @@ pub fn valid_glob_indices(glob: []const u8, indices: std.ArrayList(BraceIndex)) /// Multiple "!" characters negate the pattern multiple times. /// "\" /// Used to escape any of the special characters above. -pub fn match(glob: []const u8, path: []const u8) bool { +pub fn match(glob: []const u8, path: []const u8) MatchResult { // This algorithm is based on https://research.swtch.com/glob var state = State{}; // Store the state when we see an opening '{' brace in a stack. @@ -290,7 +302,7 @@ pub fn match(glob: []const u8, path: []const u8) bool { (glob[state.glob_index] == ',' or glob[state.glob_index] == '}')) { if (state.skipBraces(glob, false) == .Invalid) - return false; // invalid pattern! + return .no_match; // invalid pattern! } continue; @@ -321,7 +333,7 @@ pub fn match(glob: []const u8, path: []const u8) bool { while (state.glob_index < glob.len and (first or glob[state.glob_index] != ']')) { var low = glob[state.glob_index]; if (!unescape(&low, glob, &state.glob_index)) - return false; // Invalid pattern + return .no_match; // Invalid pattern state.glob_index += 1; // If there is a - and the following character is not ], @@ -332,7 +344,7 @@ pub fn match(glob: []const u8, path: []const u8) bool { state.glob_index += 1; var h = glob[state.glob_index]; if (!unescape(&h, glob, &state.glob_index)) - return false; // Invalid pattern! + return .no_match; // Invalid pattern! state.glob_index += 1; break :blk h; } else low; @@ -342,7 +354,7 @@ pub fn match(glob: []const u8, path: []const u8) bool { first = false; } if (state.glob_index >= glob.len) - return false; // Invalid pattern! + return .no_match; // Invalid pattern! state.glob_index += 1; if (is_match != class_negated) { state.path_index += 1; @@ -351,7 +363,7 @@ pub fn match(glob: []const u8, path: []const u8) bool { }, '{' => if (state.path_index < path.len) { if (brace_stack.len >= brace_stack.stack.len) - return false; // Invalid pattern! Too many nested braces. + return .no_match; // Invalid pattern! Too many nested braces. // Push old state to the stack, and reset current state. state = brace_stack.push(&state); @@ -380,7 +392,7 @@ pub fn match(glob: []const u8, path: []const u8) bool { var cc = c; // Match escaped characters as literals. if (!unescape(&cc, glob, &state.glob_index)) - return false; // Invalid pattern; + return .no_match; // Invalid pattern; const is_match = if (cc == '/') isSeparator(path[state.path_index]) @@ -416,7 +428,7 @@ pub fn match(glob: []const u8, path: []const u8) bool { if (brace_stack.len > 0) { // If in braces, find next option and reset path to index where we saw the '{' switch (state.skipBraces(glob, true)) { - .Invalid => return false, + .Invalid => return .no_match, .Comma => { state.path_index = brace_stack.last().path_index; continue; @@ -440,10 +452,10 @@ pub fn match(glob: []const u8, path: []const u8) bool { } } - return negated; + return if (negated) .negate_match else .no_match; } - return !negated; + return if (!negated) .match else .negate_no_match; } inline fn isSeparator(c: u8) bool { diff --git a/src/install/bun.lock.zig b/src/install/bun.lock.zig index 48b6290a22..b34d21e009 100644 --- a/src/install/bun.lock.zig +++ b/src/install/bun.lock.zig @@ -875,6 +875,17 @@ pub const Stringifier = struct { // need a way to detect new/deleted workspaces if (pkg_id == 0) { try writer.writeAll("\"\": {"); + const root_name = pkg_names[0].slice(buf); + if (root_name.len > 0) { + try writer.writeByte('\n'); + try incIndent(writer, indent); + try writer.print("\"name\": {}", .{ + bun.fmt.formatJSONStringUTF8(root_name, .{}), + }); + + // TODO(dylan-conway) should we save version? + any = true; + } } else { try writer.print("{}: {{", .{ bun.fmt.formatJSONStringUTF8(res.slice(buf), .{}), @@ -1625,7 +1636,7 @@ pub fn parseIntoBinaryLockfile( } } - lockfile.hoist(log, .resolvable, {}) catch |err| { + lockfile.resolve(log) catch |err| { switch (err) { error.OutOfMemory => |oom| return oom, else => { diff --git a/src/install/install.zig b/src/install/install.zig index 9379e3010d..3e1f3e89d9 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -2772,6 +2772,67 @@ pub const PackageManager = struct { last_reported_slow_lifecycle_script_at: u64 = 0, cached_tick_for_slow_lifecycle_script_logging: u64 = 0, + pub const WorkspaceFilter = union(enum) { + all, + name: []const u32, + path: []const u32, + + pub fn init(allocator: std.mem.Allocator, input: string, cwd: string, path_buf: []u8) OOM!WorkspaceFilter { + if ((input.len == 1 and input[0] == '*') or strings.eqlComptime(input, "**")) { + return .all; + } + + var remain = input; + + var prepend_negate = false; + while (remain.len > 0 and remain[0] == '!') { + prepend_negate = !prepend_negate; + remain = remain[1..]; + } + + const is_path = remain.len > 0 and remain[0] == '.'; + + const filter = if (is_path) + strings.withoutTrailingSlash(bun.path.joinAbsStringBuf(cwd, path_buf, &.{remain}, .posix)) + else + remain; + + if (filter.len == 0) { + // won't match anything + return .{ .path = &.{} }; + } + + // TODO(dylan-conway): finish encoding agnostic glob matcher so we don't + // need to convert + const len = bun.simdutf.length.utf32.from.utf8.le(filter) + @intFromBool(prepend_negate); + const buf = try allocator.alloc(u32, len); + + const result = bun.simdutf.convert.utf8.to.utf32.with_errors.le(filter, buf[@intFromBool(prepend_negate)..]); + if (!result.isSuccessful()) { + // won't match anything + return .{ .path = &.{} }; + } + + if (prepend_negate) { + buf[0] = '!'; + } + + const pattern = buf[0..len]; + + return if (is_path) + .{ .path = pattern } + else + .{ .name = pattern }; + } + + pub fn deinit(this: WorkspaceFilter, allocator: std.mem.Allocator) void { + switch (this) { + .path, .name => |pattern| allocator.free(pattern), + .all => {}, + } + } + }; + pub fn reportSlowLifecycleScripts(this: *PackageManager, log_level: Options.LogLevel) void { if (log_level == .silent) return; if (bun.getRuntimeFeatureFlag("BUN_DISABLE_SLOW_LIFECYCLE_SCRIPT_LOGGING")) { @@ -8559,6 +8620,7 @@ pub const PackageManager = struct { pub fn supportsWorkspaceFiltering(this: Subcommand) bool { return switch (this) { .outdated => true, + .install => true, // .pack => true, else => false, }; @@ -9366,7 +9428,7 @@ pub const PackageManager = struct { } else { // bun link lodash switch (manager.options.log_level) { - inline else => |log_level| try manager.updatePackageJSONAndInstallWithManager(ctx, log_level), + inline else => |log_level| try manager.updatePackageJSONAndInstallWithManager(ctx, original_cwd, log_level), } } } @@ -9539,6 +9601,7 @@ pub const PackageManager = struct { clap.parseParam("-D, --development") catch unreachable, clap.parseParam("--optional Add dependency to \"optionalDependencies\"") catch unreachable, clap.parseParam("-E, --exact Add the exact version instead of the ^range") catch unreachable, + clap.parseParam("--filter ... Install packages for the matching workspaces") catch unreachable, clap.parseParam(" ... ") catch unreachable, }); @@ -10468,7 +10531,7 @@ pub const PackageManager = struct { } switch (manager.options.log_level) { - inline else => |log_level| try manager.updatePackageJSONAndInstallWithManager(ctx, log_level), + inline else => |log_level| try manager.updatePackageJSONAndInstallWithManager(ctx, original_cwd, log_level), } if (manager.options.patch_features == .patch) { @@ -10599,6 +10662,7 @@ pub const PackageManager = struct { fn updatePackageJSONAndInstallWithManager( manager: *PackageManager, ctx: Command.Context, + original_cwd: string, comptime log_level: Options.LogLevel, ) !void { var update_requests = UpdateRequest.Array.initCapacity(manager.allocator, 64) catch bun.outOfMemory(); @@ -10632,6 +10696,7 @@ pub const PackageManager = struct { ctx, updates, manager.subcommand, + original_cwd, log_level, ); } @@ -10641,6 +10706,7 @@ pub const PackageManager = struct { ctx: Command.Context, updates: []UpdateRequest, subcommand: Subcommand, + original_cwd: string, comptime log_level: Options.LogLevel, ) !void { if (manager.log.errors > 0) { @@ -10906,7 +10972,7 @@ pub const PackageManager = struct { break :brk .{ root_package_json.source.contents, root_package_json_path_buf[0..root_package_json_path.len :0] }; }; - try manager.installWithManager(ctx, root_package_json_source, log_level); + try manager.installWithManager(ctx, root_package_json_source, original_cwd, log_level); if (subcommand == .update or subcommand == .add or subcommand == .link) { for (updates) |request| { @@ -12175,7 +12241,7 @@ pub const PackageManager = struct { // TODO(dylan-conway): print `bun install ` or `bun add ` before logs from `init`. // and cleanup install/add subcommand usage - var manager, _ = try init(ctx, cli, .install); + var manager, const original_cwd = try init(ctx, cli, .install); // switch to `bun add ` if (subcommand == .add) { @@ -12185,7 +12251,7 @@ pub const PackageManager = struct { Output.flush(); } return try switch (manager.options.log_level) { - inline else => |log_level| manager.updatePackageJSONAndInstallWithManager(ctx, log_level), + inline else => |log_level| manager.updatePackageJSONAndInstallWithManager(ctx, original_cwd, log_level), }; } @@ -12203,7 +12269,7 @@ pub const PackageManager = struct { }; try switch (manager.options.log_level) { - inline else => |log_level| manager.installWithManager(ctx, package_json_contents, log_level), + inline else => |log_level| manager.installWithManager(ctx, package_json_contents, original_cwd, log_level), }; if (manager.any_failed_to_install) { @@ -13837,12 +13903,13 @@ pub const PackageManager = struct { pub fn installPackages( this: *PackageManager, ctx: Command.Context, + original_cwd: string, comptime log_level: PackageManager.Options.LogLevel, ) !PackageInstall.Summary { const original_trees = this.lockfile.buffers.trees; const original_tree_dep_ids = this.lockfile.buffers.hoisted_dependencies; - try this.lockfile.hoist(this.log, .filter, this); + try this.lockfile.filter(this.log, this, original_cwd); defer { this.lockfile.buffers.trees = original_trees; @@ -14306,6 +14373,7 @@ pub const PackageManager = struct { manager: *PackageManager, ctx: Command.Context, root_package_json_contents: string, + original_cwd: string, comptime log_level: Options.LogLevel, ) !void { @@ -14963,6 +15031,7 @@ pub const PackageManager = struct { if (manager.options.do.install_packages) { install_summary = try manager.installPackages( ctx, + original_cwd, log_level, ); } diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index e0a0f54a4a..d57a66ae93 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -15,6 +15,7 @@ const C = bun.C; const JSAst = bun.JSAst; const TextLockfile = @import("./bun.lock.zig"); const OOM = bun.OOM; +const WorkspaceFilter = PackageManager.WorkspaceFilter; const JSLexer = bun.js_lexer; const logger = bun.logger; @@ -688,6 +689,8 @@ pub const Tree = struct { lockfile: *const Lockfile, manager: if (method == .filter) *const PackageManager else void, sort_buf: std.ArrayListUnmanaged(DependencyID) = .{}, + workspace_filters: if (method == .filter) []const WorkspaceFilter else void = if (method == .filter) &.{} else {}, + path_buf: []u8, pub fn maybeReportError(this: *@This(), comptime fmt: string, args: anytype) void { this.log.addErrorFmt(null, logger.Loc.Empty, this.allocator, fmt, args) catch {}; @@ -744,6 +747,13 @@ pub const Tree = struct { this.queue.deinit(); this.sort_buf.deinit(this.allocator); + if (comptime method == .filter) { + for (this.workspace_filters) |workspace_filter| { + workspace_filter.deinit(this.lockfile.allocator); + } + this.lockfile.allocator.free(this.workspace_filters); + } + // take over the `builder.list` pointer for only trees if (@intFromPtr(trees.ptr) != @intFromPtr(list_ptr)) { var new: [*]Tree = @ptrCast(list_ptr); @@ -859,6 +869,74 @@ pub const Tree = struct { continue; } + + if (builder.manager.subcommand == .install) { + // only do this when parent is root. workspaces are always dependencies of the root + // package, and the root package is always called with `processSubtree` + if (parent_pkg_id == 0 and builder.workspace_filters.len > 0) { + var match = false; + + for (builder.workspace_filters) |workspace_filter| { + const res_id = builder.resolutions[dep_id]; + + const pattern, const path_or_name = switch (workspace_filter) { + .name => |pattern| .{ pattern, if (builder.dependencies[dep_id].behavior.isWorkspaceOnly()) + pkg_names[res_id].slice(builder.buf()) + else + pkg_names[0].slice(builder.buf()) }, + + .path => |pattern| path: { + const res_path = if (builder.dependencies[dep_id].behavior.isWorkspaceOnly() and pkg_resolutions[res_id].tag == .workspace) + pkg_resolutions[res_id].value.workspace.slice(builder.buf()) + else + // dependnecy of the root package.json. use top level dir + FileSystem.instance.top_level_dir; + + // occupy `builder.path_buf` + var abs_res_path = strings.withoutTrailingSlash(bun.path.joinAbsStringBuf( + FileSystem.instance.top_level_dir, + builder.path_buf, + &.{res_path}, + .auto, + )); + + if (comptime Environment.isWindows) { + abs_res_path = abs_res_path[Path.windowsVolumeNameLen(abs_res_path)[0]..]; + Path.dangerouslyConvertPathToPosixInPlace(u8, builder.path_buf[0..abs_res_path.len]); + } + + break :path .{ + pattern, + abs_res_path, + }; + }, + + .all => { + match = true; + continue; + }, + }; + + switch (bun.glob.walk.matchImpl(pattern, path_or_name)) { + .match, .negate_match => match = true, + + .negate_no_match => { + // always skip if a pattern specifically says "!" + match = false; + break; + }, + + .no_match => { + // keep current + }, + } + } + + if (!match) { + continue; + } + } + } } const hoisted: HoistDependencyResult = hoisted: { @@ -1478,7 +1556,7 @@ const Cloner = struct { this.manager.clearCachedItemsDependingOnLockfileBuffer(); if (this.lockfile.packages.len != 0) { - try this.lockfile.hoist(this.log, .resolvable, {}); + try this.lockfile.resolve(this.log); } // capacity is used for calculating byte size @@ -1488,15 +1566,35 @@ const Cloner = struct { } }; +pub fn resolve( + lockfile: *Lockfile, + log: *logger.Log, +) Tree.SubtreeError!void { + return lockfile.hoist(log, .resolvable, {}, {}); +} + +pub fn filter( + lockfile: *Lockfile, + log: *logger.Log, + manager: *PackageManager, + cwd: string, +) Tree.SubtreeError!void { + return lockfile.hoist(log, .filter, manager, cwd); +} + /// Sets `buffers.trees` and `buffers.hoisted_dependencies` pub fn hoist( lockfile: *Lockfile, log: *logger.Log, comptime method: Tree.BuilderMethod, manager: if (method == .filter) *PackageManager else void, + cwd: if (method == .filter) string else void, ) Tree.SubtreeError!void { const allocator = lockfile.allocator; var slice = lockfile.packages.slice(); + + var path_buf: bun.PathBuffer = undefined; + var builder = Tree.Builder(method){ .name_hashes = slice.items(.name_hash), .queue = TreeFiller.init(allocator), @@ -1507,8 +1605,20 @@ pub fn hoist( .log = log, .lockfile = lockfile, .manager = manager, + .path_buf = &path_buf, }; + if (comptime method == .filter) { + if (manager.options.filter_patterns.len > 0) { + var filters = try std.ArrayListUnmanaged(WorkspaceFilter).initCapacity(allocator, manager.options.filter_patterns.len); + for (manager.options.filter_patterns) |pattern| { + try filters.append(allocator, try WorkspaceFilter.init(allocator, pattern, cwd, &path_buf)); + } + + builder.workspace_filters = filters.items; + } + } + try (Tree{}).processSubtree( Tree.root_dep_id, Tree.invalid_id, @@ -7125,7 +7235,7 @@ pub fn generateMetaHash(this: *Lockfile, print_name_version_string: bool, packag return digest; } -pub fn resolve(this: *Lockfile, package_name: []const u8, version: Dependency.Version) ?PackageID { +pub fn resolvePackageFromNameAndVersion(this: *Lockfile, package_name: []const u8, version: Dependency.Version) ?PackageID { const name_hash = String.Builder.stringHash(package_name); const entry = this.package_index.get(name_hash) orelse return null; const buf = this.buffers.string_bytes.items; diff --git a/src/install/migration.zig b/src/install/migration.zig index 1810598c74..d19d3e44cb 100644 --- a/src/install/migration.zig +++ b/src/install/migration.zig @@ -1017,7 +1017,7 @@ pub fn migrateNPMLockfile( return error.NotAllPackagesGotResolved; } - try this.hoist(log, .resolvable, {}); + try this.resolve(log); // if (Environment.isDebug) { // const dump_file = try std.fs.cwd().createFileZ("after-clean.json", .{}); diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig index b801b6c11c..c3435c06cb 100644 --- a/src/resolver/package_json.zig +++ b/src/resolver/package_json.zig @@ -864,7 +864,7 @@ pub const PackageJSON = struct { pm, )) |dependency_version| { if (dependency_version.value.npm.version.isExact()) { - if (pm.lockfile.resolve(package_json.name, dependency_version)) |resolved| { + if (pm.lockfile.resolvePackageFromNameAndVersion(package_json.name, dependency_version)) |resolved| { package_json.package_manager_package_id = resolved; if (resolved > 0) { break :update_dependencies; diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index f073394c1f..bf7a341467 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -583,10 +583,10 @@ pub fn relativeAlloc(allocator: std.mem.Allocator, from: []const u8, to: []const // This function is based on Go's volumeNameLen function // https://cs.opensource.google/go/go/+/refs/tags/go1.17.6:src/path/filepath/path_windows.go;l=57 // volumeNameLen returns length of the leading volume name on Windows. -fn windowsVolumeNameLen(path: []const u8) struct { usize, usize } { +pub fn windowsVolumeNameLen(path: []const u8) struct { usize, usize } { return windowsVolumeNameLenT(u8, path); } -fn windowsVolumeNameLenT(comptime T: type, path: []const T) struct { usize, usize } { +pub fn windowsVolumeNameLenT(comptime T: type, path: []const T) struct { usize, usize } { if (path.len < 2) return .{ 0, 0 }; // with drive letter const c = path[0]; diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 3ef06e90ee..d737f4af3d 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -1877,7 +1877,7 @@ pub const Resolver = struct { ) orelse break :load_module_from_cache; } - if (manager.lockfile.resolve(esm.name, dependency_version)) |id| { + if (manager.lockfile.resolvePackageFromNameAndVersion(esm.name, dependency_version)) |id| { resolved_package_id = id; } } @@ -2186,7 +2186,7 @@ pub const Resolver = struct { var pm = r.getPackageManager(); if (comptime Environment.allow_assert) { // we should never be trying to resolve a dependency that is already resolved - assert(pm.lockfile.resolve(esm.name, version) == null); + assert(pm.lockfile.resolvePackageFromNameAndVersion(esm.name, version) == null); } // Add the containing package to the lockfile diff --git a/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap b/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap index 9d2bebfe0c..c7af63e377 100644 --- a/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap @@ -140,6 +140,7 @@ exports[`text lockfile workspace sorting 1`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "dependencies": { "no-deps": "1.0.0", }, @@ -173,6 +174,7 @@ exports[`text lockfile workspace sorting 2`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "dependencies": { "no-deps": "1.0.0", }, @@ -214,6 +216,7 @@ exports[`text lockfile --frozen-lockfile 1`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "dependencies": { "a-dep": "^1.0.2", "no-deps": "^1.0.0", @@ -244,6 +247,7 @@ exports[`binaries each type of binary serializes correctly to text lockfile 1`] "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "dependencies": { "dir-bin": "./dir-bin", "file-bin": "./file-bin", @@ -270,6 +274,7 @@ exports[`binaries root resolution bins 1`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "fooooo", "dependencies": { "fooooo": ".", "no-deps": "1.0.0", @@ -290,6 +295,7 @@ exports[`hoisting text lockfile is hoisted 1`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "dependencies": { "hoist-lockfile-1": "1.0.0", "hoist-lockfile-2": "1.0.0", @@ -317,6 +323,7 @@ exports[`it should ignore peerDependencies within workspaces 1`] = ` "lockfileVersion": 0, "workspaces": { "": { + "name": "foo", "peerDependencies": { "no-deps": ">=1.0.0", }, diff --git a/test/cli/install/__snapshots__/bun-install.test.ts.snap b/test/cli/install/__snapshots__/bun-install.test.ts.snap index 96d0b00550..593bd7b0dd 100644 --- a/test/cli/install/__snapshots__/bun-install.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-install.test.ts.snap @@ -61,7 +61,9 @@ exports[`should read install.saveTextLockfile from bunfig.toml 1`] = ` "{ "lockfileVersion": 0, "workspaces": { - "": {}, + "": { + "name": "foo", + }, "packages/pkg1": { "name": "pkg-one", "version": "1.0.0", diff --git a/test/cli/install/__snapshots__/bun-lock.test.ts.snap b/test/cli/install/__snapshots__/bun-lock.test.ts.snap index 8a80309993..4f15c6c30c 100644 --- a/test/cli/install/__snapshots__/bun-lock.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-lock.test.ts.snap @@ -4,7 +4,9 @@ exports[`should escape names 1`] = ` "{ "lockfileVersion": 0, "workspaces": { - "": {}, + "": { + "name": "quote-in-dependency-name", + }, "packages/\\"": { "name": "\\"", }, @@ -23,3 +25,21 @@ exports[`should escape names 1`] = ` } " `; + +exports[`should write plaintext lockfiles 1`] = ` +"{ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "test-package", + "dependencies": { + "dummy-package": "file:./bar-0.0.2.tgz", + }, + }, + }, + "packages": { + "dummy-package": ["bar@./bar-0.0.2.tgz", {}], + } +} +" +`; diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index 5ed692c776..da074098f7 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -4025,6 +4025,7 @@ describe("binaries", () => { "lockfileVersion": 0, "workspaces": { "": { + "name": "fooooo", "dependencies": { "fooooo": ".", // out of date, no no-deps diff --git a/test/cli/install/bun-lock.test.ts b/test/cli/install/bun-lock.test.ts index 8c99857070..f60725be5f 100644 --- a/test/cli/install/bun-lock.test.ts +++ b/test/cli/install/bun-lock.test.ts @@ -45,9 +45,7 @@ it("should write plaintext lockfiles", async () => { } expect(stat.mode).toBe(mode); - expect(await file.readFile({ encoding: "utf8" })).toEqual( - `{\n \"lockfileVersion\": 0,\n \"workspaces\": {\n \"\": {\n \"dependencies\": {\n \"dummy-package\": \"file:./bar-0.0.2.tgz\",\n },\n },\n },\n \"packages\": {\n \"dummy-package\": [\"bar@./bar-0.0.2.tgz\", {}],\n }\n}\n`, - ); + expect(await file.readFile({ encoding: "utf8" })).toMatchSnapshot(); }); // won't work on windows, " is not a valid character in a filename diff --git a/test/cli/install/bun-workspaces.test.ts b/test/cli/install/bun-workspaces.test.ts index 28fed1a1a1..b282e6c1be 100644 --- a/test/cli/install/bun-workspaces.test.ts +++ b/test/cli/install/bun-workspaces.test.ts @@ -1282,3 +1282,295 @@ for (const version of versions) { }); }); } + +describe("install --filter", () => { + test("basic", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + ]); + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + exists(join(packageDir, "node_modules", "no-deps")), + ]), + ).toEqual([false, false]); + + // add workspace + await write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + exists(join(packageDir, "node_modules", "no-deps")), + ]), + ).toEqual([false, true]); + }); + + test("all but one or two", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + ]); + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!pkg2", "--save-text-lockfile"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + exists(join(packageDir, "node_modules", "pkg2")), + ]), + ).toEqual([true, { name: "no-deps", version: "2.0.0" }, false]); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + // exclude the root by name + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!root"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + exists(join(packageDir, "node_modules", "no-deps")), + exists(join(packageDir, "node_modules", "pkg1")), + exists(join(packageDir, "node_modules", "pkg2")), + ]), + ).toEqual([false, true, true, true]); + }); + + test("matched workspace depends on filtered workspace", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "root", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "1.0.0", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + dependencies: { + "pkg1": "1.0.0", + }, + }), + ), + ]); + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + expect(await exited).toBe(0); + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + exists(join(packageDir, "node_modules", "pkg1")), + exists(join(packageDir, "node_modules", "pkg2")), + ]), + ).toEqual([true, { name: "no-deps", version: "2.0.0" }, true, true]); + }); + + test("filter with a path", async () => { + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "path-pattern", + workspaces: ["packages/*"], + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + "no-deps": "2.0.0", + }, + }), + ), + ]); + + async function checkRoot() { + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + exists(join(packageDir, "node_modules", "no-deps", "package.json")), + exists(join(packageDir, "node_modules", "pkg1")), + ]), + ).toEqual([true, false, false]); + } + + async function checkWorkspace() { + expect( + await Promise.all([ + exists(join(packageDir, "node_modules", "a-dep")), + file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), + exists(join(packageDir, "node_modules", "pkg1")), + ]), + ).toEqual([false, { name: "no-deps", version: "2.0.0" }, true]); + } + + var { exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "./packages/pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + }); + + expect(await exited).toBe(0); + await checkWorkspace(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "./packages/*"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + await checkWorkspace(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!./packages/pkg1"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + await checkRoot(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!./packages/*"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + await checkRoot(); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + + ({ exited } = spawn({ + cmd: [bunExe(), "install", "--filter", "!./"], + cwd: packageDir, + stdout: "ignore", + stderr: "pipe", + env, + })); + + expect(await exited).toBe(0); + await checkWorkspace(); + }); +});