aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2023-03-20 05:57:23 -0700
committerGravatar GitHub <noreply@github.com> 2023-03-20 05:57:23 -0700
commit1a25af5e3dde2389c747e18d565cedf39c79a43d (patch)
treeea07c80c6a6d30a78d1ef930c4a9249a84a40cb8
parent343721627ee65caf504d67c5046b0da94bfeba72 (diff)
downloadbun-1a25af5e3dde2389c747e18d565cedf39c79a43d.tar.gz
bun-1a25af5e3dde2389c747e18d565cedf39c79a43d.tar.zst
bun-1a25af5e3dde2389c747e18d565cedf39c79a43d.zip
Implement simple `workspaces` glob support in bun install (#2435)
* [bun install] Implement `packages/*`-style globs * Fix incorrect assertion * :nail_care: * remove extraneous console.log * Fix pointer to stack memory * Add a test with a scoped package name from a glob workspace * Fixup --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
-rw-r--r--src/install/lockfile.zig562
-rw-r--r--src/string_immutable.zig2
-rw-r--r--test/cli/install/bad-workspace.test.ts6
-rw-r--r--test/cli/install/bun-add.test.ts4
-rw-r--r--test/cli/install/bun-install.test.ts55
5 files changed, 410 insertions, 219 deletions
diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig
index 8d8892736..f7e872d4b 100644
--- a/src/install/lockfile.zig
+++ b/src/install/lockfile.zig
@@ -2298,8 +2298,8 @@ pub const Package = extern struct {
workspace_path: ?String,
external_name: ExternalString,
version: string,
- key: Expr,
- value: Expr,
+ key_loc: logger.Loc,
+ value_loc: logger.Loc,
) !?Dependency {
const external_version = string_builder.append(String, version);
var buf = lockfile.buffers.string_bytes.items;
@@ -2352,7 +2352,7 @@ pub const Package = extern struct {
dependency_version.value.workspace = path;
var workspace_entry = try lockfile.workspace_paths.getOrPut(allocator, @truncate(u32, external_name.hash));
if (workspace_entry.found_existing) {
- log.addErrorFmt(&source, key.loc, allocator, "Workspace name \"{s}\" already exists", .{
+ log.addErrorFmt(&source, logger.Loc.Empty, allocator, "Workspace name \"{s}\" already exists", .{
external_name.slice(buf),
}) catch {};
return error.InstallFailed;
@@ -2392,7 +2392,7 @@ pub const Package = extern struct {
try log.addRangeErrorFmtWithNotes(
&source,
- source.rangeOfString(key.loc),
+ source.rangeOfString(key_loc),
lockfile.allocator,
notes,
"Duplicate dependency: \"{s}\" specified in package.json",
@@ -2401,14 +2401,54 @@ pub const Package = extern struct {
}
}
- entry.value_ptr.* = value.loc;
+ entry.value_ptr.* = value_loc;
}
return this_dep;
}
+ const WorkspaceIterator = struct {
+ pub const Entry = struct {
+ path: []const u8 = "",
+ name: []const u8 = "",
+ };
+ };
+
+ fn processWorkspaceName(
+ allocator: std.mem.Allocator,
+ workspace_allocator: std.mem.Allocator,
+ dir: std.fs.Dir,
+ path: []const u8,
+ path_buf: *[bun.MAX_PATH_BYTES]u8,
+ name_to_copy: *[1024]u8,
+ log: *logger.Log,
+ ) !WorkspaceIterator.Entry {
+ const path_to_use = if (path.len == 0) "package.json" else brk: {
+ const paths = [_]string{ path, "package.json" };
+ break :brk bun.path.joinStringBuf(path_buf, &paths, .auto);
+ };
+ var workspace_file = try dir.openFile(path_to_use, .{ .mode = .read_only });
+ defer workspace_file.close();
+
+ const workspace_bytes = try workspace_file.readToEndAlloc(workspace_allocator, std.math.maxInt(usize));
+ defer workspace_allocator.free(workspace_bytes);
+ const workspace_source = logger.Source.initPathString(path, workspace_bytes);
+
+ var workspace_json = try json_parser.PackageJSONVersionChecker.init(allocator, &workspace_source, log);
+
+ _ = try workspace_json.parseExpr();
+ if (!workspace_json.has_found_name) {
+ return error.MissingPackageName;
+ }
+ bun.copy(u8, name_to_copy[0..], workspace_json.found_name);
+ return WorkspaceIterator.Entry{
+ .name = name_to_copy[0..workspace_json.found_name.len],
+ .path = path_to_use,
+ };
+ }
+
fn processWorkspaceNamesArray(
- workspace_names_ptr: *[]string,
+ workspace_names: *bun.StringArrayHashMap(string),
allocator: Allocator,
log: *logger.Log,
arr: *JSAst.E.Array,
@@ -2416,19 +2456,26 @@ pub const Package = extern struct {
loc: logger.Loc,
string_builder: *StringBuilder,
) !u32 {
- var workspace_names = try allocator.alloc(string, arr.items.len);
- defer workspace_names_ptr.* = workspace_names;
+ workspace_names.* = bun.StringArrayHashMap(string).init(allocator);
+ workspace_names.ensureTotalCapacity(arr.items.len) catch unreachable;
var fallback = std.heap.stackFallback(1024, allocator);
var workspace_allocator = fallback.get();
+ var workspace_name_buf = allocator.create([1024]u8) catch unreachable;
+ defer allocator.destroy(workspace_name_buf);
const orig_msgs_len = log.msgs.items.len;
- for (arr.slice(), workspace_names) |item, *name| {
+ var asterisked_workspace_paths = std.ArrayList(string).init(allocator);
+ defer asterisked_workspace_paths.deinit();
+ var filepath_buf = allocator.create([bun.MAX_PATH_BYTES]u8) catch unreachable;
+ defer allocator.destroy(filepath_buf);
+
+ for (arr.slice()) |item| {
defer fallback.fixed_buffer_allocator.reset();
- const path = item.asString(allocator) orelse {
+ var input_path = item.asString(allocator) orelse {
log.addErrorFmt(source, item.loc, allocator,
- \\Workspaces expects an array of strings, e.g.
+ \\Workspaces expects an array of strings, like:
\\"workspaces": [
\\ "path/to/package"
\\]
@@ -2436,97 +2483,239 @@ pub const Package = extern struct {
return error.InvalidPackageJSON;
};
- var workspace_dir = std.fs.cwd().openIterableDir(path, .{}) catch |err| {
- if (err == error.FileNotFound) {
- log.addErrorFmt(
- source,
- item.loc,
- allocator,
- "Workspace not found \"{s}\" in \"{s}\"",
- .{
- path,
- std.os.getcwd(allocator.alloc(u8, bun.MAX_PATH_BYTES) catch unreachable) catch unreachable,
- },
+ if (strings.containsChar(input_path, '*')) {
+ if (strings.contains(input_path, "**")) {
+ log.addError(source, item.loc,
+ \\TODO multi level globs. For now, try something like "packages/*"
) catch {};
- } else log.addErrorFmt(
- source,
- item.loc,
- allocator,
- "{s} opening workspace package \"{s}\" from \"{s}\"",
- .{
- @errorName(err),
- path,
- std.os.getcwd(allocator.alloc(u8, bun.MAX_PATH_BYTES) catch unreachable) catch unreachable,
- },
- ) catch {};
- name.* = "";
- // report errors for multiple workspaces
- continue;
- };
- defer workspace_dir.close();
+ continue;
+ }
- var workspace_file = workspace_dir.dir.openFile("package.json", .{ .mode = .read_only }) catch |err| {
- if (err == error.FileNotFound) {
- log.addErrorFmt(
- source,
- item.loc,
- allocator,
- "package.json not found in workspace package \"{s}\" from \"{s}\"",
- .{ path, std.os.getcwd(allocator.alloc(u8, bun.MAX_PATH_BYTES) catch unreachable) catch unreachable },
+ const without_trailing_slash = strings.withoutTrailingSlash(input_path);
+
+ if (!strings.endsWithComptime(without_trailing_slash, "/*") and !strings.eqlComptime(without_trailing_slash, "*")) {
+ log.addError(source, item.loc,
+ \\TODO glob star * in the middle of a path. For now, try something like "packages/*", at the end of the path.
) catch {};
- } else log.addErrorFmt(
- source,
- item.loc,
- allocator,
- "{s} opening package.json for workspace package \"{s}\" from \"{s}\"",
- .{ @errorName(err), path, std.os.getcwd(allocator.alloc(u8, bun.MAX_PATH_BYTES) catch unreachable) catch unreachable },
- ) catch {};
- name.* = "";
- // report errors for multiple workspaces
- continue;
- };
- defer workspace_file.close();
-
- const workspace_bytes = workspace_file.readToEndAlloc(workspace_allocator, std.math.maxInt(usize)) catch |err| {
- log.addErrorFmt(
- source,
- item.loc,
- allocator,
- "{s} reading package.json for workspace package \"{s}\" from \"{s}\"",
- .{ @errorName(err), path, std.os.getcwd(allocator.alloc(u8, bun.MAX_PATH_BYTES) catch unreachable) catch unreachable },
- ) catch {};
- name.* = "";
- // report errors for multiple workspaces
+ continue;
+ }
+
+ asterisked_workspace_paths.append(without_trailing_slash) catch unreachable;
continue;
- };
- defer workspace_allocator.free(workspace_bytes);
- const workspace_source = logger.Source.initPathString(path, workspace_bytes);
-
- var workspace_json = try json_parser.PackageJSONVersionChecker.init(allocator, &workspace_source, log);
-
- _ = try workspace_json.parseExpr();
- if (!workspace_json.has_found_name) {
- log.addErrorFmt(
- source,
- loc,
- allocator,
- "Missing \"name\" from package.json in {s}",
- .{workspace_source.path.text},
+ } else if (strings.containsAny(input_path, "!{}[]")) {
+ log.addError(source, item.loc,
+ \\TODO fancy glob patterns. For now, try something like "packages/*"
) catch {};
- // report errors for multiple workspaces
continue;
}
- const workspace_name = workspace_json.found_name;
+ const workspace_entry = processWorkspaceName(
+ allocator,
+ workspace_allocator,
+ std.fs.cwd(),
+ input_path,
+ filepath_buf,
+ workspace_name_buf,
+ log,
+ ) catch |err| {
+ switch (err) {
+ error.FileNotFound => {
+ log.addErrorFmt(
+ source,
+ item.loc,
+ allocator,
+ "Workspace not found \"{s}\"",
+ .{input_path},
+ ) catch {};
+ },
+ error.MissingPackageName => {
+ log.addErrorFmt(
+ source,
+ loc,
+ allocator,
+ "Missing \"name\" from package.json in {s}",
+ .{input_path},
+ ) catch {};
+ },
+ else => {
+ log.addErrorFmt(
+ source,
+ item.loc,
+ allocator,
+ "{s} reading package.json for workspace package \"{s}\" from \"{s}\"",
+ .{ @errorName(err), input_path, std.os.getcwd(allocator.alloc(u8, bun.MAX_PATH_BYTES) catch unreachable) catch unreachable },
+ ) catch {};
+ },
+ }
+ continue;
+ };
+
+ if (workspace_entry.name.len == 0) continue;
- string_builder.count(workspace_name);
- string_builder.count(path);
+ string_builder.count(workspace_entry.name);
+ string_builder.count(input_path);
string_builder.cap += bun.MAX_PATH_BYTES;
- name.* = try allocator.dupe(u8, workspace_name);
+
+ var result = try workspace_names.getOrPut(input_path);
+ if (!result.found_existing) {
+ result.key_ptr.* = try allocator.dupe(u8, input_path);
+ result.value_ptr.* = try allocator.dupe(u8, workspace_entry.name);
+ }
+ }
+
+ if (asterisked_workspace_paths.items.len > 0) {
+ // max path bytes is not enough in real codebases
+ var second_buf = allocator.create([4096]u8) catch unreachable;
+ var second_buf_fixed = std.heap.FixedBufferAllocator.init(second_buf);
+ defer allocator.destroy(second_buf);
+
+ for (asterisked_workspace_paths.items) |user_path| {
+ var dir_prefix = strings.withoutLeadingSlash(user_path);
+ dir_prefix = user_path[0 .. strings.indexOfChar(dir_prefix, '*') orelse continue];
+
+ if (dir_prefix.len == 0 or
+ strings.eqlComptime(dir_prefix, ".") or
+ strings.eqlComptime(dir_prefix, "./"))
+ {
+ dir_prefix = ".";
+ }
+
+ const entries_option = FileSystem.instance.fs.readDirectory(
+ dir_prefix,
+ null,
+ ) catch |err| switch (err) {
+ error.FileNotFound => {
+ log.addWarningFmt(
+ source,
+ loc,
+ allocator,
+ "workspaces directory prefix not found \"{s}\"",
+ .{dir_prefix},
+ ) catch {};
+ continue;
+ },
+ error.NotDir => {
+ log.addWarningFmt(
+ source,
+ loc,
+ allocator,
+ "workspaces directory prefix is not a directory \"{s}\"",
+ .{dir_prefix},
+ ) catch {};
+ continue;
+ },
+ else => continue,
+ };
+ if (entries_option.* != .entries) continue;
+ var entries = entries_option.entries.data.iterator();
+ const skipped_names = &[_][]const u8{ "node_modules", ".git" };
+
+ while (entries.next()) |entry_iter| {
+ const name = entry_iter.key_ptr.*;
+ if (strings.eqlAnyComptime(name, skipped_names))
+ continue;
+ var entry: *FileSystem.Entry = entry_iter.value_ptr.*;
+ if (entry.kind(&Fs.FileSystem.instance.fs) != .dir) continue;
+
+ var parts = [2]string{ entry.dir, entry.base() };
+ var entry_path = Path.joinAbsStringBufZ(
+ Fs.FileSystem.instance.topLevelDirWithoutTrailingSlash(),
+ filepath_buf,
+ &parts,
+ .auto,
+ );
+
+ if (entry.cache.fd == 0) {
+ entry.cache.fd = std.os.openatZ(
+ std.os.AT.FDCWD,
+ entry_path,
+ std.os.O.DIRECTORY | std.os.O.CLOEXEC | std.os.O.NOCTTY,
+ 0,
+ ) catch continue;
+ }
+
+ const dir_fd = entry.cache.fd;
+ std.debug.assert(dir_fd != 0); // kind() should've opened
+ defer fallback.fixed_buffer_allocator.reset();
+
+ const workspace_entry = processWorkspaceName(
+ allocator,
+ workspace_allocator,
+ std.fs.Dir{
+ .fd = dir_fd,
+ },
+ "",
+ filepath_buf,
+ workspace_name_buf,
+ log,
+ ) catch |err| {
+ switch (err) {
+ error.FileNotFound, error.PermissionDenied => continue,
+ error.MissingPackageName => {
+ log.addErrorFmt(
+ source,
+ logger.Loc.Empty,
+ allocator,
+ "Missing \"name\" from package.json in {s}" ++ std.fs.path.sep_str ++ "{s}",
+ .{ entry.dir, entry.base() },
+ ) catch {};
+ },
+ else => {
+ log.addErrorFmt(
+ source,
+ logger.Loc.Empty,
+ allocator,
+ "{s} reading package.json for workspace package \"{s}\" from \"{s}\"",
+ .{ @errorName(err), entry.dir, entry.base() },
+ ) catch {};
+ },
+ }
+
+ continue;
+ };
+
+ if (workspace_entry.name.len == 0) continue;
+
+ string_builder.count(workspace_entry.name);
+ second_buf_fixed.reset();
+ const relative = std.fs.path.relative(
+ second_buf_fixed.allocator(),
+ Fs.FileSystem.instance.top_level_dir,
+ bun.span(entry_path),
+ ) catch unreachable;
+
+ string_builder.count(relative);
+ string_builder.cap += bun.MAX_PATH_BYTES;
+
+ var result = try workspace_names.getOrPut(relative);
+ if (!result.found_existing) {
+ result.value_ptr.* = try allocator.dupe(
+ u8,
+ workspace_entry.name,
+ );
+ result.key_ptr.* = try allocator.dupe(u8, relative);
+ }
+ }
+ }
}
if (orig_msgs_len != log.msgs.items.len) return error.InstallFailed;
- return @truncate(u32, arr.items.len);
+
+ // Sort the names for determinism
+ workspace_names.sort(struct {
+ values: []const string,
+ pub fn lessThan(
+ self: @This(),
+ a: usize,
+ b: usize,
+ ) bool {
+ return std.mem.order(u8, self.values[a], self.values[b]) == .lt;
+ }
+ }{
+ .values = workspace_names.values(),
+ });
+
+ return @truncate(u32, workspace_names.count());
}
fn parseWithJSON(
@@ -2641,6 +2830,7 @@ pub const Package = extern struct {
@as(usize, @boolToInt(features.workspaces))
]DependencyGroup = undefined;
var out_group_i: usize = 0;
+
if (features.dependencies) {
out_groups[out_group_i] = DependencyGroup.dependencies;
out_group_i += 1;
@@ -2668,7 +2858,15 @@ pub const Package = extern struct {
break :brk out_groups;
};
- var workspace_names: []string = &.{};
+ var workspace_names = bun.StringArrayHashMap(string).init(allocator);
+ defer {
+ for (workspace_names.values(), workspace_names.keys()) |name, path| {
+ allocator.free(name);
+ allocator.free(path);
+ }
+ workspace_names.deinit();
+ }
+
inline for (dependency_groups) |group| {
if (json.asProperty(group.prop)) |dependencies_q| brk: {
switch (dependencies_q.expr.data) {
@@ -2910,130 +3108,76 @@ pub const Package = extern struct {
total_dependencies_count = 0;
const in_workspace = lockfile.workspace_paths.contains(@truncate(u32, package.name_hash));
- inline for (dependency_groups) |group| {
- if (json.asProperty(group.prop)) |dependencies_q| brk: {
- switch (dependencies_q.expr.data) {
- .e_array => |arr| {
- if (arr.items.len == 0) break :brk;
- for (arr.slice(), workspace_names) |item, name| {
- defer allocator.free(name);
-
- const external_name = string_builder.append(ExternalString, name);
- const path = item.asString(allocator).?;
-
- if (try parseDependency(
- lockfile,
- allocator,
- log,
- source,
- group,
- &string_builder,
- features,
- package_dependencies,
- total_dependencies_count,
- in_workspace,
- .workspace,
- null,
- external_name,
- path,
- item,
- item,
- )) |dep| {
- package_dependencies[total_dependencies_count] = dep;
- total_dependencies_count += 1;
- }
- }
+ inline for (dependency_groups) |group| {
+ if (group.behavior.isWorkspace()) {
+ for (workspace_names.values(), workspace_names.keys()) |name, path| {
+ const external_name = string_builder.append(ExternalString, name);
- allocator.free(workspace_names);
- },
- .e_object => |obj| {
- if (group.behavior.isWorkspace()) {
- // yarn workspaces expects a "workspaces" property shaped like this:
- //
- // "workspaces": {
- // "packages": [
- // "path/to/package"
- // ]
- // }
- //
- if (obj.get("packages")) |packages_q| {
- if (packages_q.data == .e_array) {
- var arr = packages_q.data.e_array;
- if (arr.items.len == 0) break :brk;
-
- for (arr.slice(), workspace_names) |item, name| {
- defer allocator.free(name);
-
- const external_name = string_builder.append(ExternalString, name);
- const path = item.asString(allocator).?;
-
- if (try parseDependency(
- lockfile,
- allocator,
- log,
- source,
- group,
- &string_builder,
- features,
- package_dependencies,
- total_dependencies_count,
- in_workspace,
- .workspace,
- null,
- external_name,
- path,
- item,
- item,
- )) |dep| {
- package_dependencies[total_dependencies_count] = dep;
- total_dependencies_count += 1;
- }
- }
-
- allocator.free(workspace_names);
- break :brk;
+ if (try parseDependency(
+ lockfile,
+ allocator,
+ log,
+ source,
+ group,
+ &string_builder,
+ features,
+ package_dependencies,
+ total_dependencies_count,
+ in_workspace,
+ .workspace,
+ null,
+ external_name,
+ path,
+ logger.Loc.Empty,
+ logger.Loc.Empty,
+ )) |dep| {
+ package_dependencies[total_dependencies_count] = dep;
+ total_dependencies_count += 1;
+ }
+ }
+ } else {
+ if (json.asProperty(group.prop)) |dependencies_q| {
+ switch (dependencies_q.expr.data) {
+ .e_object => |obj| {
+ for (obj.properties.slice()) |item| {
+ const key = item.key.?;
+ const value = item.value.?;
+ const external_name = string_builder.append(ExternalString, key.asString(allocator).?);
+ const version = value.asString(allocator) orelse "";
+ var tag: ?Dependency.Version.Tag = null;
+ var workspace_path: ?String = null;
+
+ if (lockfile.workspace_paths.get(@truncate(u32, external_name.hash))) |path| {
+ tag = .workspace;
+ workspace_path = path;
}
- }
- }
- for (obj.properties.slice()) |item| {
- const key = item.key.?;
- const value = item.value.?;
- const external_name = string_builder.append(ExternalString, key.asString(allocator).?);
- const version = value.asString(allocator) orelse "";
- var tag: ?Dependency.Version.Tag = null;
- var workspace_path: ?String = null;
-
- if (lockfile.workspace_paths.get(@truncate(u32, external_name.hash))) |path| {
- tag = .workspace;
- workspace_path = path;
- }
-
- if (try parseDependency(
- lockfile,
- allocator,
- log,
- source,
- group,
- &string_builder,
- features,
- package_dependencies,
- total_dependencies_count,
- in_workspace,
- tag,
- workspace_path,
- external_name,
- version,
- key,
- value,
- )) |dep| {
- package_dependencies[total_dependencies_count] = dep;
- total_dependencies_count += 1;
+ if (try parseDependency(
+ lockfile,
+ allocator,
+ log,
+ source,
+ group,
+ &string_builder,
+ features,
+ package_dependencies,
+ total_dependencies_count,
+ in_workspace,
+ tag,
+ workspace_path,
+ external_name,
+ version,
+ key.loc,
+ value.loc,
+ )) |dep| {
+ package_dependencies[total_dependencies_count] = dep;
+ total_dependencies_count += 1;
+ }
}
- }
- },
- else => unreachable,
+ },
+ else => unreachable,
+ }
}
}
}
diff --git a/src/string_immutable.zig b/src/string_immutable.zig
index d393577fc..321abb9f7 100644
--- a/src/string_immutable.zig
+++ b/src/string_immutable.zig
@@ -3192,7 +3192,7 @@ pub fn indexOfCharZ(sliceZ: [:0]const u8, char: u8) ?u63 {
const pos = @ptrToInt(ptr) - @ptrToInt(sliceZ.ptr);
if (comptime Environment.allow_assert)
- std.debug.assert(@ptrToInt(sliceZ.ptr) >= @ptrToInt(ptr) and
+ std.debug.assert(@ptrToInt(sliceZ.ptr) <= @ptrToInt(ptr) and
@ptrToInt(ptr) < @ptrToInt(sliceZ.ptr + sliceZ.len) and
pos <= sliceZ.len);
diff --git a/test/cli/install/bad-workspace.test.ts b/test/cli/install/bad-workspace.test.ts
index 1feafc2b0..bd8f0b24d 100644
--- a/test/cli/install/bad-workspace.test.ts
+++ b/test/cli/install/bad-workspace.test.ts
@@ -12,7 +12,7 @@ test("bad workspace path", () => {
JSON.stringify(
{
name: "hey",
- workspaces: ["i-dont-exist", "*/i-have-a-star-and-i-dont-exist"],
+ workspaces: ["i-dont-exist", "**/i-have-a-2-stars-and-i-dont-exist", "*/i-have-a-star-and-i-dont-exist"],
},
null,
2,
@@ -28,7 +28,9 @@ test("bad workspace path", () => {
const text = stderr!.toString();
expect(text).toContain('Workspace not found "i-dont-exist"');
- expect(text).toContain('Workspace not found "*/i-have-a-star-and-i-dont-exist"');
+ expect(text).toContain("multi level globs");
+ expect(text).toContain("glob star * in the middle of a path");
+
expect(exitCode).toBe(1);
rmSync(cwd, { recursive: true, force: true });
});
diff --git a/test/cli/install/bun-add.test.ts b/test/cli/install/bun-add.test.ts
index d1222993b..310341560 100644
--- a/test/cli/install/bun-add.test.ts
+++ b/test/cli/install/bun-add.test.ts
@@ -366,7 +366,7 @@ it("should add dependency alongside workspaces", async () => {
JSON.stringify({
name: "foo",
version: "0.0.1",
- workspaces: ["packages/bar"],
+ workspaces: ["packages/*"],
}),
);
await mkdir(join(package_dir, "packages", "bar"), { recursive: true });
@@ -417,7 +417,7 @@ it("should add dependency alongside workspaces", async () => {
expect(await file(join(package_dir, "package.json")).json()).toEqual({
name: "foo",
version: "0.0.1",
- workspaces: ["packages/bar"],
+ workspaces: ["packages/*"],
dependencies: {
baz: "^0.0.3",
},
diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts
index d722a15fb..bdb5053c3 100644
--- a/test/cli/install/bun-install.test.ts
+++ b/test/cli/install/bun-install.test.ts
@@ -212,7 +212,7 @@ it("should handle workspaces", async () => {
JSON.stringify({
name: "Foo",
version: "0.0.1",
- workspaces: ["bar"],
+ workspaces: ["bar", "packages/*"],
}),
);
await mkdir(join(package_dir, "bar"));
@@ -223,6 +223,34 @@ it("should handle workspaces", async () => {
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"],
cwd: package_dir,
@@ -237,14 +265,30 @@ it("should handle workspaces", async () => {
expect(stdout).toBeDefined();
const out = await new Response(stdout).text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
+ " + @org/nominally-scoped@workspace:packages/nominally-scoped",
+ " + Asterisk@workspace:packages/asterisk",
+ " + AsteriskTheSecond@workspace:packages/second-asterisk",
" + Bar@workspace:bar",
"",
- " 1 packages installed",
+ " 4 packages installed",
]);
expect(await exited).toBe(0);
expect(requested).toBe(0);
- expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar"]);
+ expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([
+ ".cache",
+ "@org",
+ "Asterisk",
+ "AsteriskTheSecond",
+ "Bar",
+ ]);
expect(await readlink(join(package_dir, "node_modules", "Bar"))).toBe(join("..", "bar"));
+ expect(await readlink(join(package_dir, "node_modules", "Asterisk"))).toBe(join("..", "packages", "asterisk"));
+ expect(await readlink(join(package_dir, "node_modules", "AsteriskTheSecond"))).toBe(
+ join("..", "packages", "second-asterisk"),
+ );
+ expect(await readlink(join(package_dir, "node_modules", "@org", "nominally-scoped"))).toBe(
+ join("..", "..", "packages", "nominally-scoped"),
+ );
await access(join(package_dir, "bun.lockb"));
});
@@ -2290,8 +2334,9 @@ it("should report error on duplicated workspace packages", async () => {
"",
'error: Workspace name "moo" already exists',
'{"name":"foo","version":"0.0.1","workspaces":["bar","baz"]}',
- " ^",
- `${package_dir}/package.json:1:53 52`,
+ // we don't have a name location anymore
+ "^",
+ `${package_dir}/package.json:1:1 0`,
"",
]);
expect(stdout).toBeDefined();