diff options
| -rw-r--r-- | src/cli/package_manager_command.zig | 115 | ||||
| -rw-r--r-- | test/bun.js/install/bun-pm.test.ts | 248 |
2 files changed, 321 insertions, 42 deletions
diff --git a/src/cli/package_manager_command.zig b/src/cli/package_manager_command.zig index ec9936fbf..fb4309f1b 100644 --- a/src/cli/package_manager_command.zig +++ b/src/cli/package_manager_command.zig @@ -1,21 +1,20 @@ -const Command = @import("../cli.zig").Command; -const PackageManager = @import("../install/install.zig").PackageManager; -const ComamndLineArguments = PackageManager.CommandLineArguments; const std = @import("std"); -const strings = @import("bun").strings; +const bun = @import("bun"); +const Global = bun.Global; +const Output = bun.Output; +const string = bun.string; +const strings = bun.strings; +const Command = @import("../cli.zig").Command; +const Fs = @import("../fs.zig"); +const Dependency = @import("../install/dependency.zig"); +const Install = @import("../install/install.zig"); +const PackageID = Install.PackageID; +const DependencyID = Install.DependencyID; +const PackageManager = Install.PackageManager; const Lockfile = @import("../install/lockfile.zig"); const NodeModulesFolder = Lockfile.Tree.NodeModulesFolder; -const PackageID = @import("../install/install.zig").PackageID; -const DependencyID = @import("../install/install.zig").DependencyID; -const PackageInstaller = @import("../install/install.zig").PackageInstaller; -const Global = @import("bun").Global; -const Output = @import("bun").Output; -const Fs = @import("../fs.zig"); const Path = @import("../resolver/resolve_path.zig"); -const bun = @import("bun"); -const StringBuilder = bun.StringBuilder; -const string = bun.string; -const stringZ = bun.stringZ; +const String = @import("../install/semver.zig").String; fn handleLoadLockfileErrors(load_lockfile: Lockfile.LoadFromDiskResult, pm: *PackageManager) void { if (load_lockfile == .not_found) { @@ -31,6 +30,19 @@ fn handleLoadLockfileErrors(load_lockfile: Lockfile.LoadFromDiskResult, pm: *Pac } } +const ByName = struct { + dependencies: []const Dependency, + buf: []const u8, + + pub fn isLessThan(ctx: ByName, lhs: DependencyID, rhs: DependencyID) bool { + return strings.cmpStringsAsc( + {}, + ctx.dependencies[lhs].name.slice(ctx.buf), + ctx.dependencies[rhs].name.slice(ctx.buf), + ); + } +}; + pub const PackageManagerCommand = struct { pub fn printHelp(_: std.mem.Allocator) void {} pub fn printHash(ctx: Command.Context, lockfile_: []const u8) !void { @@ -172,17 +184,18 @@ pub const PackageManagerCommand = struct { var directories = std.ArrayList(NodeModulesFolder).init(ctx.allocator); defer directories.deinit(); while (iterator.nextNodeModulesFolder()) |node_modules| { - const path = try ctx.allocator.alloc(u8, node_modules.relative_path.len); + const path_len = node_modules.relative_path.len; + const path = try ctx.allocator.alloc(u8, path_len + 1); bun.copy(u8, path, node_modules.relative_path); + path[path_len] = 0; const dependencies = try ctx.allocator.alloc(DependencyID, node_modules.dependencies.len); - bun.copy(PackageID, dependencies, node_modules.dependencies); + bun.copy(DependencyID, dependencies, node_modules.dependencies); - const folder = NodeModulesFolder{ - .relative_path = @ptrCast(stringZ, path), + try directories.append(.{ + .relative_path = path[0..path_len :0], .dependencies = dependencies, - }; - directories.append(folder) catch unreachable; + }); } const first_directory = directories.orderedRemove(0); @@ -190,31 +203,42 @@ pub const PackageManagerCommand = struct { // TODO: find max depth beforehand var more_packages = [_]bool{false} ** 16; if (first_directory.dependencies.len > 1) more_packages[0] = true; - const recurse = strings.leftHasAnyInRight(args, &.{ "-A", "-a", "--all" }); - if (recurse) { - printNodeModulesFolderStructure(&first_directory, null, 0, &directories, lockfile, more_packages); - Output.enableBuffering(); + if (strings.leftHasAnyInRight(args, &.{ "-A", "-a", "--all" })) { + try printNodeModulesFolderStructure(&first_directory, null, 0, &directories, lockfile, more_packages); } else { var cwd_buf: [bun.MAX_PATH_BYTES]u8 = undefined; const path = std.os.getcwd(&cwd_buf) catch { Output.prettyErrorln("<r><red>error<r>: Could not get current working directory", .{}); Global.exit(1); }; - const package_ids = lockfile.packages.items(.resolutions)[0].get(lockfile.buffers.resolutions.items); + const dependencies = lockfile.buffers.dependencies.items; + const slice = lockfile.packages.slice(); + const resolutions = slice.items(.resolution); + const root_deps = slice.items(.dependencies)[0]; - Output.println("{s} node_modules ({d})", .{ path, package_ids.len }); - Output.enableBuffering(); - const names = lockfile.packages.items(.name); + Output.println("{s} node_modules ({d})", .{ path, dependencies.len }); const string_bytes = lockfile.buffers.string_bytes.items; + const sorted_dependencies = try ctx.allocator.alloc(DependencyID, root_deps.len); + defer ctx.allocator.free(sorted_dependencies); + for (sorted_dependencies, 0..) |*dep, i| { + dep.* = @truncate(DependencyID, root_deps.off + i); + } + std.sort.sort(DependencyID, sorted_dependencies, ByName{ + .dependencies = dependencies, + .buf = string_bytes, + }, ByName.isLessThan); - for (package_ids, 0..) |package_id, i| { + for (sorted_dependencies, 0..) |dependency_id, index| { + const package_id = lockfile.buffers.resolutions.items[dependency_id]; if (package_id >= lockfile.packages.len) continue; + const name = dependencies[dependency_id].name.slice(string_bytes); + const resolution = resolutions[package_id].fmt(string_bytes); - if (i == package_ids.len - 1) { - Output.prettyln("<d>└──<r> {s}<r><d>@{any}<r>\n", .{ names[package_id].slice(string_bytes), lockfile.packages.items(.resolution)[package_id].fmt(string_bytes) }); + if (index < sorted_dependencies.len - 1) { + Output.prettyln("<d>├──<r> {s}<r><d>@{any}<r>\n", .{ name, resolution }); } else { - Output.prettyln("<d>├──<r> {s}<r><d>@{any}<r>\n", .{ names[package_id].slice(string_bytes), lockfile.packages.items(.resolution)[package_id].fmt(string_bytes) }); + Output.prettyln("<d>└──<r> {s}<r><d>@{any}<r>\n", .{ name, resolution }); } } } @@ -255,7 +279,7 @@ fn printNodeModulesFolderStructure( directories: *std.ArrayList(NodeModulesFolder), lockfile: *Lockfile, more_packages_: [16]bool, -) void { +) !void { const allocator = lockfile.allocator; var more_packages = more_packages_; const resolutions = lockfile.packages.items(.resolution); @@ -292,7 +316,7 @@ fn printNodeModulesFolderStructure( } } } - const directory_version = std.fmt.bufPrint(&resolution_buf, "{}", .{resolutions[id].fmt(string_bytes)}) catch unreachable; + const directory_version = try std.fmt.bufPrint(&resolution_buf, "{}", .{resolutions[id].fmt(string_bytes)}); if (std.mem.indexOf(u8, path, "node_modules")) |j| { Output.prettyln("{s}<d>@{s}<r>", .{ path[0 .. j - 1], directory_version }); } else { @@ -308,15 +332,22 @@ fn printNodeModulesFolderStructure( } } - for (directory.dependencies, 0..) |dependency_id, index| { - const package_name_ = lockfile.buffers.dependencies.items[dependency_id].name.slice(string_bytes); - const package_name = allocator.dupe(u8, package_name_) catch unreachable; - defer allocator.free(package_name); + const dependencies = lockfile.buffers.dependencies.items; + const sorted_dependencies = try allocator.alloc(DependencyID, directory.dependencies.len); + defer allocator.free(sorted_dependencies); + bun.copy(DependencyID, sorted_dependencies, directory.dependencies); + std.sort.sort(DependencyID, sorted_dependencies, ByName{ + .dependencies = dependencies, + .buf = string_bytes, + }, ByName.isLessThan); + + for (sorted_dependencies, 0..) |dependency_id, index| { + const package_name = dependencies[dependency_id].name.slice(string_bytes); - var possible_path = std.fmt.allocPrint(allocator, "{s}/{s}/node_modules", .{ directory.relative_path, package_name }) catch unreachable; + var possible_path = try std.fmt.allocPrint(allocator, "{s}/{s}/node_modules", .{ directory.relative_path, package_name }); defer allocator.free(possible_path); - if (index + 1 == directory.dependencies.len) { + if (index + 1 == sorted_dependencies.len) { more_packages[depth] = false; } @@ -338,7 +369,7 @@ fn printNodeModulesFolderStructure( } more_packages[new_depth] = true; - printNodeModulesFolderStructure(&next, package_id, new_depth, directories, lockfile, more_packages); + try printNodeModulesFolderStructure(&next, package_id, new_depth, directories, lockfile, more_packages); } } @@ -360,7 +391,7 @@ fn printNodeModulesFolderStructure( } var resolution_buf: [512]u8 = undefined; - const package_version = std.fmt.bufPrint(&resolution_buf, "{}", .{resolutions[package_id].fmt(string_bytes)}) catch unreachable; + const package_version = try std.fmt.bufPrint(&resolution_buf, "{}", .{resolutions[package_id].fmt(string_bytes)}); Output.prettyln("{s}<d>@{s}<r>", .{ package_name, package_version }); } } diff --git a/test/bun.js/install/bun-pm.test.ts b/test/bun.js/install/bun-pm.test.ts new file mode 100644 index 000000000..54bfc534c --- /dev/null +++ b/test/bun.js/install/bun-pm.test.ts @@ -0,0 +1,248 @@ +import { spawn } from "bun"; +import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; +import { bunExe } from "bunExe"; +import { bunEnv as env } from "bunEnv"; +import { mkdir, writeFile } from "fs/promises"; +import { join } from "path"; +import { + dummyAfterAll, + dummyAfterEach, + dummyBeforeAll, + dummyBeforeEach, + dummyRegistry, + package_dir, + requested, + root_url, + setHandler, +} from "./dummy.registry"; + +beforeAll(dummyBeforeAll); +afterAll(dummyAfterAll); +beforeEach(dummyBeforeEach); +afterEach(dummyAfterEach); + +it("should list top-level dependency", async () => { + const urls: string[] = []; + setHandler(dummyRegistry(urls)); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + moo: "./moo", + }, + }), + ); + await mkdir(join(package_dir, "moo")); + await writeFile( + join(package_dir, "moo", "package.json"), + JSON.stringify({ + name: "moo", + version: "0.1.0", + dependencies: { + bar: "latest", + }, + }), + ); + expect( + await spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }).exited, + ).toBe(0); + expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); + expect(requested).toBe(2); + urls.length = 0; + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "ls"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + expect(await new Response(stderr).text()).toBe(""); + expect(stdout).toBeDefined(); + expect(await new Response(stdout).text()).toBe(`${package_dir} node_modules (2) +└── moo@moo +`); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([]); + expect(requested).toBe(2); +}); + +it("should list all dependencies", async () => { + const urls: string[] = []; + setHandler(dummyRegistry(urls)); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + moo: "./moo", + }, + }), + ); + await mkdir(join(package_dir, "moo")); + await writeFile( + join(package_dir, "moo", "package.json"), + JSON.stringify({ + name: "moo", + version: "0.1.0", + dependencies: { + bar: "latest", + }, + }), + ); + expect( + await spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }).exited, + ).toBe(0); + expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); + expect(requested).toBe(2); + urls.length = 0; + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "ls", "--all"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + expect(await new Response(stderr).text()).toBe(""); + expect(stdout).toBeDefined(); + expect(await new Response(stdout).text()).toBe(`${package_dir} node_modules +├── bar@0.0.2 +└── moo@moo +`); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([]); + expect(requested).toBe(2); +}); + +it("should list top-level aliased dependency", async () => { + const urls: string[] = []; + setHandler(dummyRegistry(urls)); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "moo-1": "./moo", + }, + }), + ); + await mkdir(join(package_dir, "moo")); + await writeFile( + join(package_dir, "moo", "package.json"), + JSON.stringify({ + name: "moo", + version: "0.1.0", + dependencies: { + "bar-1": "npm:bar", + }, + }), + ); + expect( + await spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }).exited, + ).toBe(0); + expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); + expect(requested).toBe(2); + urls.length = 0; + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "ls"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + expect(await new Response(stderr).text()).toBe(""); + expect(stdout).toBeDefined(); + expect(await new Response(stdout).text()).toBe(`${package_dir} node_modules (2) +└── moo-1@moo +`); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([]); + expect(requested).toBe(2); +}); + +it("should list aliased dependencies", async () => { + const urls: string[] = []; + setHandler(dummyRegistry(urls)); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "moo-1": "./moo", + }, + }), + ); + await mkdir(join(package_dir, "moo")); + await writeFile( + join(package_dir, "moo", "package.json"), + JSON.stringify({ + name: "moo", + version: "0.1.0", + dependencies: { + "bar-1": "npm:bar", + }, + }), + ); + expect( + await spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }).exited, + ).toBe(0); + expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); + expect(requested).toBe(2); + urls.length = 0; + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "ls", "--all"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + expect(await new Response(stderr).text()).toBe(""); + expect(stdout).toBeDefined(); + expect(await new Response(stdout).text()).toBe(`${package_dir} node_modules +└── moo-1@moo + └── bar-1@0.0.2 +`); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([]); + expect(requested).toBe(2); +}); |
