diff --git a/src/bun.js/api/glob.zig b/src/bun.js/api/glob.zig index f25381c077..0e3f37375c 100644 --- a/src/bun.js/api/glob.zig +++ b/src/bun.js/api/glob.zig @@ -371,7 +371,7 @@ pub fn match(this: *Glob, globalThis: *JSGlobalObject, callframe: *jsc.CallFrame var str = try str_arg.toSlice(globalThis, arena.allocator()); defer str.deinit(); - return jsc.JSValue.jsBoolean(globImpl.match(arena.allocator(), this.pattern, str.slice()).matches()); + return jsc.JSValue.jsBoolean(bun.glob.match(this.pattern, str.slice()).matches()); } pub fn convertUtf8(codepoints: *std.ArrayList(u32), pattern: []const u8) !void { @@ -390,12 +390,10 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Arena = std.heap.ArenaAllocator; -const globImpl = @import("../../glob.zig"); -const GlobWalker = globImpl.BunGlobWalker; - const bun = @import("bun"); const BunString = bun.String; const CodepointIterator = bun.strings.UnsignedCodepointIterator; +const GlobWalker = bun.glob.BunGlobWalker; const jsc = bun.jsc; const JSGlobalObject = jsc.JSGlobalObject; diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index d34e0c22bb..62bfe42229 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -127,9 +127,8 @@ pub const TestRunner = struct { const file_path = this.files.items(.source)[file_id].path.text; // Check if the file path matches any of the glob patterns - const glob = @import("../../glob.zig"); for (glob_patterns) |pattern| { - const result = glob.match(this.allocator, pattern, file_path); + const result = bun.glob.match(pattern, file_path); if (result == .match) return true; } return false; diff --git a/src/cli/filter_arg.zig b/src/cli/filter_arg.zig index 0d072805d6..3d0e00c6e6 100644 --- a/src/cli/filter_arg.zig +++ b/src/cli/filter_arg.zig @@ -22,7 +22,7 @@ fn globIgnoreFn(val: []const u8) bool { return false; } -const GlobWalker = Glob.GlobWalker(globIgnoreFn, Glob.walk.DirEntryAccessor, false); +const GlobWalker = glob.GlobWalker(globIgnoreFn, glob.walk.DirEntryAccessor, false); pub fn getCandidatePackagePatterns(allocator: std.mem.Allocator, log: *bun.logger.Log, out_patterns: *std.ArrayList([]u8), workdir_: []const u8, root_buf: *bun.PathBuffer) ![]const u8 { bun.ast.Expr.Data.Store.create(); @@ -177,7 +177,7 @@ pub const FilterSet = struct { pub fn matchesPath(self: *const FilterSet, path: []const u8) bool { for (self.filters) |filter| { - if (Glob.walk.matchImpl(self.allocator, filter.pattern, path).matches()) { + if (glob.match(filter.pattern, path).matches()) { return true; } } @@ -190,7 +190,7 @@ pub const FilterSet = struct { .name => name, .path => path, }; - if (Glob.walk.matchImpl(self.allocator, filter.pattern, target).matches()) { + if (glob.match(filter.pattern, target).matches()) { return true; } } @@ -275,11 +275,11 @@ pub const PackageFilterIterator = struct { const string = []const u8; -const Glob = @import("../glob.zig"); const std = @import("std"); const bun = @import("bun"); const Global = bun.Global; const JSON = bun.json; const Output = bun.Output; +const glob = bun.glob; const strings = bun.strings; diff --git a/src/cli/outdated_command.zig b/src/cli/outdated_command.zig index 98b2cf0fc4..91f5cd64e7 100644 --- a/src/cli/outdated_command.zig +++ b/src/cli/outdated_command.zig @@ -190,14 +190,14 @@ pub const OutdatedCommand = struct { const abs_res_path = path.joinAbsStringBuf(FileSystem.instance.top_level_dir, &path_buf, &[_]string{res_path}, .posix); - if (!glob.walk.matchImpl(allocator, pattern, strings.withoutTrailingSlash(abs_res_path)).matches()) { + if (!glob.match(pattern, strings.withoutTrailingSlash(abs_res_path)).matches()) { break :matched false; } }, .name => |pattern| { const name = pkg_names[workspace_pkg_id].slice(string_buf); - if (!glob.walk.matchImpl(allocator, pattern, name).matches()) { + if (!glob.match(pattern, name).matches()) { break :matched false; } }, @@ -403,7 +403,7 @@ pub const OutdatedCommand = struct { .path => unreachable, .name => |name_pattern| { if (name_pattern.len == 0) continue; - if (!glob.walk.matchImpl(bun.default_allocator, name_pattern, dep.name.slice(string_buf)).matches()) { + if (!glob.match(name_pattern, dep.name.slice(string_buf)).matches()) { break :match false; } }, diff --git a/src/cli/pack_command.zig b/src/cli/pack_command.zig index d8b2f139d4..5c2b7e3d0e 100644 --- a/src/cli/pack_command.zig +++ b/src/cli/pack_command.zig @@ -293,7 +293,7 @@ pub const PackCommand = struct { // normally the behavior of `index.js` and `**/index.js` are the same, // but includes require `**/` const match_path = if (include.flags.@"leading **/") entry_name else entry_subpath; - switch (glob.walk.matchImpl(allocator, include.glob.slice(), match_path)) { + switch (glob.match(include.glob.slice(), match_path)) { .match => included = true, .negate_no_match, .negate_match => unreachable, else => {}, @@ -310,7 +310,7 @@ pub const PackCommand = struct { const match_path = if (exclude.flags.@"leading **/") entry_name else entry_subpath; // NOTE: These patterns have `!` so `.match` logic is // inverted here - switch (glob.walk.matchImpl(allocator, exclude.glob.slice(), match_path)) { + switch (glob.match(exclude.glob.slice(), match_path)) { .negate_no_match => included = false, else => {}, } @@ -1034,7 +1034,7 @@ pub const PackCommand = struct { // check default ignores that only apply to the root project directory for (root_default_ignore_patterns) |pattern| { - switch (glob.walk.matchImpl(bun.default_allocator, pattern, entry_name)) { + switch (glob.match(pattern, entry_name)) { .match => { // cannot be reversed return .{ @@ -1061,7 +1061,7 @@ pub const PackCommand = struct { for (default_ignore_patterns) |pattern_info| { const pattern, const can_override = pattern_info; - switch (glob.walk.matchImpl(bun.default_allocator, pattern, entry_name)) { + switch (glob.match(pattern, entry_name)) { .match => { if (can_override) { ignored = true; @@ -1103,7 +1103,7 @@ pub const PackCommand = struct { if (pattern.flags.dirs_only and entry.kind != .directory) continue; const match_path = if (pattern.flags.rel_path) rel else entry_name; - switch (glob.walk.matchImpl(bun.default_allocator, pattern.glob.slice(), match_path)) { + switch (glob.match(pattern.glob.slice(), match_path)) { .match => { ignored = true; ignore_pattern = pattern.glob.slice(); diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index c74e88db93..28c11d2bba 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -1014,7 +1014,7 @@ pub const CommandLineReporter = struct { if (opts.ignore_patterns.len > 0) { var should_ignore = false; for (opts.ignore_patterns) |pattern| { - if (bun.glob.match(bun.default_allocator, pattern, relative_path).matches()) { + if (bun.glob.match(pattern, relative_path).matches()) { should_ignore = true; break; } @@ -1134,7 +1134,7 @@ pub const CommandLineReporter = struct { var should_ignore = false; for (opts.ignore_patterns) |pattern| { - if (bun.glob.match(bun.default_allocator, pattern, relative_path).matches()) { + if (bun.glob.match(pattern, relative_path).matches()) { should_ignore = true; break; } diff --git a/src/cli/update_interactive_command.zig b/src/cli/update_interactive_command.zig index 4aaa36d1a7..873b25bc5b 100644 --- a/src/cli/update_interactive_command.zig +++ b/src/cli/update_interactive_command.zig @@ -602,14 +602,14 @@ pub const UpdateInteractiveCommand = struct { const abs_res_path = path.joinAbsStringBuf(FileSystem.instance.top_level_dir, &path_buf, &[_]string{res_path}, .posix); - if (!glob.walk.matchImpl(allocator, pattern, strings.withoutTrailingSlash(abs_res_path)).matches()) { + if (!glob.match(pattern, strings.withoutTrailingSlash(abs_res_path)).matches()) { break :matched false; } }, .name => |pattern| { const name = pkg_names[workspace_pkg_id].slice(string_buf); - if (!glob.walk.matchImpl(allocator, pattern, name).matches()) { + if (!glob.match(pattern, name).matches()) { break :matched false; } }, diff --git a/src/glob.zig b/src/glob.zig index 5519351638..07b48c5c18 100644 --- a/src/glob.zig +++ b/src/glob.zig @@ -1,8 +1,40 @@ +pub const match = @import("./glob/match.zig").match; pub const walk = @import("./glob/GlobWalker.zig"); -pub const match_impl = @import("./glob/match.zig"); -pub const match = match_impl.match; -pub const detectGlobSyntax = match_impl.detectGlobSyntax; - pub const GlobWalker = walk.GlobWalker_; pub const BunGlobWalker = GlobWalker(null, walk.SyscallAccessor, false); pub const BunGlobWalkerZ = GlobWalker(null, walk.SyscallAccessor, true); + +/// Returns true if the given string contains glob syntax, +/// excluding those escaped with backslashes +/// TODO: this doesn't play nicely with Windows directory separator and +/// backslashing, should we just require the user to supply posix filepaths? +pub fn detectGlobSyntax(potential_pattern: []const u8) bool { + // Negation only allowed in the beginning of the pattern + if (potential_pattern.len > 0 and potential_pattern[0] == '!') return true; + + // In descending order of how popular the token is + const SPECIAL_SYNTAX: [4]u8 = comptime [_]u8{ '*', '{', '[', '?' }; + + inline for (SPECIAL_SYNTAX) |token| { + var slice = potential_pattern[0..]; + while (slice.len > 0) { + if (std.mem.indexOfScalar(u8, slice, token)) |idx| { + // Check for even number of backslashes preceding the + // token to know that it's not escaped + var i = idx; + var backslash_count: u16 = 0; + + while (i > 0 and potential_pattern[i - 1] == '\\') : (i -= 1) { + backslash_count += 1; + } + + if (backslash_count % 2 == 0) return true; + slice = slice[idx + 1 ..]; + } else break; + } + } + + return false; +} + +const std = @import("std"); diff --git a/src/glob/GlobWalker.zig b/src/glob/GlobWalker.zig index 96a484c663..38f4e30aa1 100644 --- a/src/glob/GlobWalker.zig +++ b/src/glob/GlobWalker.zig @@ -1324,8 +1324,7 @@ pub fn GlobWalker_( } fn matchPatternSlow(this: *GlobWalker, pattern_component: *Component, filepath: []const u8) bool { - return match( - this.arena.allocator(), + return bun.glob.match( pattern_component.patternSlice(this.pattern), filepath, ).matches(); @@ -1686,11 +1685,8 @@ pub fn matchWildcardLiteral(literal: []const u8, path: []const u8) bool { return std.mem.eql(u8, literal, path); } -pub const matchImpl = match; - const DirIterator = @import("../bun.js/node/dir_iterator.zig"); const ResolvePath = @import("../resolver/resolve_path.zig"); -const match = @import("./match.zig").match; const bun = @import("bun"); const BunString = bun.String; diff --git a/src/glob/match.zig b/src/glob/match.zig index 391a86f455..36d50919cb 100644 --- a/src/glob/match.zig +++ b/src/glob/match.zig @@ -33,7 +33,7 @@ const Brace = struct { }; const BraceStack = bun.BoundedArray(Brace, 10); -pub const MatchResult = enum { +const MatchResult = enum { no_match, match, @@ -116,7 +116,7 @@ const Wildcard = struct { /// Used to escape any of the special characters above. // TODO: consider just taking arena and resetting to initial state, // all usages of this function pass in Arena.allocator() -pub fn match(_: Allocator, glob: []const u8, path: []const u8) MatchResult { +pub fn match(glob: []const u8, path: []const u8) MatchResult { var state = State{}; var negated = false; @@ -486,39 +486,6 @@ inline fn skipGlobstars(glob: []const u8, glob_index: *u32) void { glob_index.* -= 2; } -/// Returns true if the given string contains glob syntax, -/// excluding those escaped with backslashes -/// TODO: this doesn't play nicely with Windows directory separator and -/// backslashing, should we just require the user to supply posix filepaths? -pub fn detectGlobSyntax(potential_pattern: []const u8) bool { - // Negation only allowed in the beginning of the pattern - if (potential_pattern.len > 0 and potential_pattern[0] == '!') return true; - - // In descending order of how popular the token is - const SPECIAL_SYNTAX: [4]u8 = comptime [_]u8{ '*', '{', '[', '?' }; - - inline for (SPECIAL_SYNTAX) |token| { - var slice = potential_pattern[0..]; - while (slice.len > 0) { - if (std.mem.indexOfScalar(u8, slice, token)) |idx| { - // Check for even number of backslashes preceding the - // token to know that it's not escaped - var i = idx; - var backslash_count: u16 = 0; - - while (i > 0 and potential_pattern[i - 1] == '\\') : (i -= 1) { - backslash_count += 1; - } - - if (backslash_count % 2 == 0) return true; - slice = slice[idx + 1 ..]; - } else break; - } - } - - return false; -} - const BraceIndex = struct { start: u32 = 0, end: u32 = 0, @@ -526,4 +493,3 @@ const BraceIndex = struct { const bun = @import("bun"); const std = @import("std"); -const Allocator = std.mem.Allocator; diff --git a/src/install/PackageManager/install_with_manager.zig b/src/install/PackageManager/install_with_manager.zig index 90d016cd8c..cb4663e145 100644 --- a/src/install/PackageManager/install_with_manager.zig +++ b/src/install/PackageManager/install_with_manager.zig @@ -1064,7 +1064,7 @@ pub fn getWorkspaceFilters(manager: *PackageManager, original_cwd: []const u8) ! }, }; - switch (bun.glob.walk.matchImpl(manager.allocator, pattern, path_or_name)) { + switch (bun.glob.match(pattern, path_or_name)) { .match, .negate_match => install_root_dependencies = true, .negate_no_match => { diff --git a/src/install/lockfile/Package/WorkspaceMap.zig b/src/install/lockfile/Package/WorkspaceMap.zig index f0cbc1279e..2373d9db1d 100644 --- a/src/install/lockfile/Package/WorkspaceMap.zig +++ b/src/install/lockfile/Package/WorkspaceMap.zig @@ -130,7 +130,7 @@ pub fn processNamesArray( if (input_path.len == 0 or input_path.len == 1 and input_path[0] == '.') continue; - if (Glob.detectGlobSyntax(input_path)) { + if (glob.detectGlobSyntax(input_path)) { bun.handleOom(workspace_globs.append(input_path)); continue; } @@ -215,7 +215,7 @@ pub fn processNamesArray( if (workspace_globs.items.len > 0) { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); - for (workspace_globs.items) |user_pattern| { + for (workspace_globs.items, 0..) |user_pattern, i| { defer _ = arena.reset(.retain_capacity); const glob_pattern = if (user_pattern.len == 0) "package.json" else brk: { @@ -253,7 +253,7 @@ pub fn processNamesArray( return error.GlobError; } - while (switch (try iter.next()) { + next_match: while (switch (try iter.next()) { .result => |r| r, .err => |e| { log.addErrorFmt( @@ -271,6 +271,28 @@ pub fn processNamesArray( // skip root package.json if (strings.eqlComptime(matched_path, "package.json")) continue; + { + const matched_path_without_package_json = strings.withoutTrailingSlash(strings.withoutSuffixComptime(matched_path, "package.json")); + + // check if it's negated by any remaining patterns + for (workspace_globs.items[i + 1 ..]) |next_pattern| { + switch (bun.glob.match(next_pattern, matched_path_without_package_json)) { + .no_match, + .match, + .negate_match, + => {}, + + .negate_no_match => { + debug("skipping negated path: {s}, {s}\n", .{ + matched_path_without_package_json, + next_pattern, + }); + continue :next_match; + }, + } + } + } + debug("matched path: {s}, dirname: {s}\n", .{ matched_path, entry_dir }); const abs_package_json_path = Path.joinAbsStringBufZ( @@ -375,7 +397,7 @@ fn ignoredWorkspacePaths(path: []const u8) bool { } return false; } -const GlobWalker = Glob.GlobWalker(ignoredWorkspacePaths, Glob.walk.SyscallAccessor, false); +const GlobWalker = glob.GlobWalker(ignoredWorkspacePaths, glob.walk.SyscallAccessor, false); const string = []const u8; const debug = Output.scoped(.Lockfile, .hidden); @@ -386,10 +408,10 @@ const Allocator = std.mem.Allocator; const bun = @import("bun"); const Environment = bun.Environment; -const Glob = bun.glob; const JSAst = bun.ast; const Output = bun.Output; const Path = bun.path; +const glob = bun.glob; const logger = bun.logger; const strings = bun.strings; diff --git a/src/install/lockfile/Tree.zig b/src/install/lockfile/Tree.zig index 2b5f958659..a75feae070 100644 --- a/src/install/lockfile/Tree.zig +++ b/src/install/lockfile/Tree.zig @@ -408,7 +408,7 @@ pub fn isFilteredDependencyOrWorkspace( }, }; - switch (bun.glob.match(undefined, pattern, name_or_path)) { + switch (bun.glob.match(pattern, name_or_path)) { .match, .negate_match => workspace_matched = true, .negate_no_match => { diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig index e118f86f89..1ccf7529ca 100644 --- a/src/resolver/package_json.zig +++ b/src/resolver/package_json.zig @@ -150,7 +150,7 @@ pub const PackageJSON = struct { defer bun.default_allocator.free(normalized_path); for (glob_list.items) |pattern| { - if (glob.match(bun.default_allocator, pattern, normalized_path).matches()) { + if (glob.match(pattern, normalized_path).matches()) { return true; } } @@ -166,7 +166,7 @@ pub const PackageJSON = struct { defer bun.default_allocator.free(normalized_path); for (mixed.globs.items) |pattern| { - if (glob.match(bun.default_allocator, pattern, normalized_path).matches()) { + if (glob.match(pattern, normalized_path).matches()) { return true; } } diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index b14bcb5b99..04fe05f620 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -67,7 +67,7 @@ pub const WorkPool = jsc.WorkPool; pub const Pipe = [2]bun.FileDescriptor; pub const SmolList = shell.SmolList; -pub const GlobWalker = Glob.BunGlobWalkerZ; +pub const GlobWalker = bun.glob.BunGlobWalkerZ; pub const stdin_no = 0; pub const stdout_no = 1; @@ -1957,7 +1957,6 @@ pub fn unreachableState(context: []const u8, state: []const u8) noreturn { return bun.Output.panic("Bun shell has reached an unreachable state \"{s}\" in the {s} context. This indicates a bug, please open a GitHub issue.", .{ state, context }); } -const Glob = @import("../glob.zig"); const builtin = @import("builtin"); const WTFStringImplStruct = @import("../string.zig").WTFStringImplStruct; diff --git a/src/shell/shell.zig b/src/shell/shell.zig index e8ded9f1ae..7759256915 100644 --- a/src/shell/shell.zig +++ b/src/shell/shell.zig @@ -17,7 +17,7 @@ pub const IOReader = Interpreter.IOReader; pub const Yield = @import("./Yield.zig").Yield; pub const unreachableState = interpret.unreachableState; -const GlobWalker = Glob.GlobWalker_(null, true); +const GlobWalker = bun.glob.GlobWalker(null, true); // const GlobWalker = Glob.BunGlobWalker; pub const SUBSHELL_TODO_ERROR = "Subshells are not implemented, please open GitHub issue!"; @@ -4429,7 +4429,6 @@ pub const TestingAPIs = struct { pub const ShellSubprocess = @import("./subproc.zig").ShellSubprocess; -const Glob = @import("../glob.zig"); const Syscall = @import("../sys.zig"); const builtin = @import("builtin"); diff --git a/test/cli/install/bun-workspaces.test.ts b/test/cli/install/bun-workspaces.test.ts index 219c2b50ac..a5a2ba94a5 100644 --- a/test/cli/install/bun-workspaces.test.ts +++ b/test/cli/install/bun-workspaces.test.ts @@ -136,6 +136,44 @@ test("dependency on workspace without version in package.json", async () => { } }, 20_000); +test("allowing negative workspace patterns", async () => { + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "root", + workspaces: ["packages/*", "!packages/pkg2"], + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + dependencies: { + "doesnt-exist-oops": "1.2.3", + }, + }), + ), + ]); + + const { exited } = await runBunInstall(env, packageDir); + expect(await exited).toBe(0); + + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.0.0", + }); +}); + test("dependency on same name as workspace and dist-tag", async () => { await Promise.all([ write(