mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 06:12:08 +00:00
fix(install): make negative workspace patterns work (#23229)
### What does this PR do? It's common for monorepos to exclude portions of a large glob ```json "workspaces": [ "packages/**", "!packages/**/test/**", "!packages/**/template/**" ], ``` closes #4621 (note: patterns like `"packages/!(*-standalone)"` will need to be written `"!packages/*-standalone"`) ### How did you verify your code works? Manually tested https://github.com/opentiny/tiny-engine, and added a new workspace test. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
40
src/glob.zig
40
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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user