diff options
-rw-r--r-- | src/bun.js/module_loader.zig | 4 | ||||
-rw-r--r-- | src/install/dependency.zig | 84 | ||||
-rw-r--r-- | src/install/install.zig | 210 | ||||
-rw-r--r-- | src/install/lockfile.zig | 46 | ||||
-rw-r--r-- | src/install/resolvers/folder_resolver.zig | 2 | ||||
-rw-r--r-- | src/resolver/package_json.zig | 12 | ||||
-rw-r--r-- | src/resolver/resolver.zig | 9 | ||||
-rw-r--r-- | test/bun.js/install/bun-install.test.ts | 154 |
8 files changed, 265 insertions, 256 deletions
diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 91081786b..01ae2771a 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -621,9 +621,9 @@ pub const ModuleLoader = struct { .{ result.name, result.url }, ), error.DistTagNotFound, error.NoMatchingVersion => brk: { - const prefix: []const u8 = if (result.err == error.NoMatchingVersion and result.version.tag == .npm and result.version.value.npm.isExact()) + const prefix: []const u8 = if (result.err == error.NoMatchingVersion and result.version.tag == .npm and result.version.value.npm.version.isExact()) "Version not found" - else if (result.version.tag == .npm and !result.version.value.npm.isExact()) + else if (result.version.tag == .npm and !result.version.value.npm.version.isExact()) "No matching version found" else "No match found"; diff --git a/src/install/dependency.zig b/src/install/dependency.zig index af28c99ba..44ca91d51 100644 --- a/src/install/dependency.zig +++ b/src/install/dependency.zig @@ -88,12 +88,14 @@ pub fn cloneWithDifferentBuffers(this: Dependency, name_buf: []const u8, version const out_slice = builder.lockfile.buffers.string_bytes.items; const new_literal = builder.append(String, this.version.literal.slice(version_buf)); const sliced = new_literal.sliced(out_slice); + const new_name = builder.append(String, this.name.slice(name_buf)); return Dependency{ .name_hash = this.name_hash, - .name = builder.append(String, this.name.slice(name_buf)), + .name = new_name, .version = Dependency.parseWithTag( builder.lockfile.allocator, + new_name, new_literal.slice(out_slice), this.version.tag, &sliced, @@ -120,13 +122,14 @@ pub fn toDependency( this: External, ctx: Context, ) Dependency { + const name = String{ + .bytes = this[0..8].*, + }; return Dependency{ - .name = String{ - .bytes = this[0..8].*, - }, + .name = name, .name_hash = @bitCast(u64, this[8..16].*), .behavior = @intToEnum(Dependency.Behavior, this[16]), - .version = Dependency.Version.toVersion(this[17..this.len].*, ctx), + .version = Dependency.Version.toVersion(name, this[17..this.len].*, ctx), }; } @@ -147,7 +150,7 @@ pub const Version = struct { pub fn deinit(this: *Version) void { switch (this.tag) { .npm => { - this.value.npm.deinit(); + this.value.npm.version.deinit(); }, else => {}, } @@ -195,6 +198,7 @@ pub const Version = struct { pub const External = [9]u8; pub fn toVersion( + alias: String, bytes: Version.External, ctx: Dependency.Context, ) Dependency.Version { @@ -203,6 +207,7 @@ pub const Version = struct { const sliced = &slice.sliced(ctx.buffer); return Dependency.parseWithTag( ctx.allocator, + alias, sliced.slice, tag, sliced, @@ -231,7 +236,7 @@ pub const Version = struct { // if the two versions are identical as strings, it should often be faster to compare that than the actual semver version // semver ranges involve a ton of pointer chasing .npm => strings.eql(lhs.literal.slice(lhs_buf), rhs.literal.slice(rhs_buf)) or - lhs.value.npm.eql(rhs.value.npm), + lhs.value.npm.eql(rhs.value.npm, lhs_buf, rhs_buf), .folder, .dist_tag => lhs.literal.eql(rhs.literal, lhs_buf, rhs_buf), .tarball => lhs.value.tarball.eql(rhs.value.tarball, lhs_buf, rhs_buf), .symlink => lhs.value.symlink.eql(rhs.value.symlink, lhs_buf, rhs_buf), @@ -443,10 +448,19 @@ pub const Version = struct { } }; + const NpmInfo = struct { + name: String, + version: Semver.Query.Group, + + fn eql(this: NpmInfo, that: NpmInfo, this_buf: []const u8, that_buf: []const u8) bool { + return this.name.eql(that.name, this_buf, that_buf) and this.version.eql(that.version); + } + }; + pub const Value = union { uninitialized: void, - npm: Semver.Query.Group, + npm: NpmInfo, dist_tag: String, tarball: URI, folder: String, @@ -481,40 +495,29 @@ pub fn eqlResolved(a: Dependency, b: Dependency) bool { pub inline fn parse( allocator: std.mem.Allocator, + alias: String, dependency: string, sliced: *const SlicedString, log: ?*logger.Log, ) ?Version { - return parseWithOptionalTag(allocator, dependency, null, sliced, log); + return parseWithOptionalTag(allocator, alias, dependency, null, sliced, log); } pub fn parseWithOptionalTag( allocator: std.mem.Allocator, + alias: String, dependency_: string, tag_or_null: ?Dependency.Version.Tag, sliced: *const SlicedString, log: ?*logger.Log, ) ?Version { var dependency = std.mem.trimLeft(u8, dependency_, " \t\n\r"); - if (dependency.len == 0) return null; - const tag = tag_or_null orelse Version.Tag.infer(dependency); - - if (tag == .npm and strings.hasPrefixComptime(dependency, "npm:")) { - dependency = dependency[4..]; - } - - // Strip single leading v - // v1.0.0 -> 1.0.0 - // note: "vx" is valid, it becomes "x". "yarn add react@vx" -> "yarn add react@x" -> "yarn add react@17.0.2" - if (tag == .npm and dependency.len > 1 and dependency[0] == 'v') { - dependency = dependency[1..]; - } - return parseWithTag( allocator, + alias, dependency, - tag, + tag_or_null orelse Version.Tag.infer(dependency), sliced, log, ); @@ -522,6 +525,7 @@ pub fn parseWithOptionalTag( pub fn parseWithTag( allocator: std.mem.Allocator, + alias: String, dependency: string, tag: Dependency.Version.Tag, sliced: *const SlicedString, @@ -529,10 +533,31 @@ pub fn parseWithTag( ) ?Version { switch (tag) { .npm => { + var input = dependency; + const name = if (strings.hasPrefixComptime(input, "npm:")) sliced.sub(brk: { + var str = input["npm:".len..]; + var i: usize = 0; + while (i < str.len) : (i += 1) { + if (str[i] == '@') { + input = str[i + 1 ..]; + break :brk str[0..i]; + } + } + input = str[i..]; + break :brk str[0..i]; + }).value() else alias; + + // Strip single leading v + // v1.0.0 -> 1.0.0 + // note: "vx" is valid, it becomes "x". "yarn add react@vx" -> "yarn add react@x" -> "yarn add react@17.0.2" + if (input.len > 1 and input[0] == 'v') { + input = input[1..]; + } + const version = Semver.Query.parse( allocator, - dependency, - sliced.sub(dependency), + input, + sliced.sub(input), ) catch |err| { if (log_) |log| log.addErrorFmt(null, logger.Loc.Empty, allocator, "{s} parsing dependency \"{s}\"", .{ @errorName(err), dependency }) catch unreachable; return null; @@ -540,7 +565,12 @@ pub fn parseWithTag( return Version{ .literal = sliced.value(), - .value = .{ .npm = version }, + .value = .{ + .npm = .{ + .name = name, + .version = version, + }, + }, .tag = .npm, }; }, diff --git a/src/install/install.zig b/src/install/install.zig index 6a402cfbc..86e0ca532 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -635,89 +635,6 @@ const PackageInstall = struct { package_version: string, file_count: u32 = 0, - threadlocal var package_json_checker: json_parser.PackageJSONVersionChecker = undefined; - - pub const Context = struct { - metas: []const Lockfile.Package.Meta, - names: []const String, - resolutions: []const Resolution, - string_buf: []const u8, - channel: PackageInstall.Task.Channel = undefined, - skip_verify: bool = false, - progress: *Progress = undefined, - cache_dir: std.fs.IterableDir = undefined, - allocator: std.mem.Allocator, - }; - - pub const Task = struct { - task: ThreadPool.Task = .{ .callback = &callback }, - result: Result = Result{ .pending = void{} }, - package_install: PackageInstall = undefined, - package_id: PackageID, - ctx: *PackageInstall.Context, - destination_dir: std.fs.IterableDir, - - pub const Channel = sync.Channel(*PackageInstall.Task, .{ .Static = 1024 }); - - pub fn callback(task: *ThreadPool.Task) void { - Output.Source.configureThread(); - defer Output.flush(); - - var this: *PackageInstall.Task = @fieldParentPtr(PackageInstall.Task, "task", task); - var ctx = this.ctx; - - var destination_dir_subpath_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - var cache_dir_subpath_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - const name = ctx.names[this.package_id].slice(ctx.string_buf); - const resolution = ctx.resolutions[this.package_id]; - std.mem.copy(u8, &destination_dir_subpath_buf, name); - destination_dir_subpath_buf[name.len] = 0; - var destination_dir_subpath: [:0]u8 = destination_dir_subpath_buf[0..name.len :0]; - var resolution_buf: [512]u8 = undefined; - var resolution_label = std.fmt.bufPrint(&resolution_buf, "{}", .{resolution.fmt(ctx.string_buf)}) catch unreachable; - - this.package_install = PackageInstall{ - .cache_dir = undefined, - .cache_dir_subpath = undefined, - .progress = ctx.progress, - - .destination_dir = this.destination_dir, - .destination_dir_subpath = destination_dir_subpath, - .destination_dir_subpath_buf = &destination_dir_subpath_buf, - .allocator = ctx.allocator, - .package_name = name, - .package_version = resolution_label, - }; - - switch (resolution.tag) { - .npm => { - this.package_install.cache_dir_subpath = this.manager.cachedNPMPackageFolderName(name, resolution.value.npm); - this.package_install.cache_dir = this.manager.getCacheDirectory(); - }, - .folder => { - var folder_buf = &cache_dir_subpath_buf; - const folder = resolution.value.folder.slice(ctx.string_buf); - std.mem.copy(u8, folder_buf, "../" ++ std.fs.path.sep_str); - std.mem.copy(u8, folder_buf["../".len..], folder); - folder_buf["../".len + folder.len] = 0; - this.package_install.cache_dir_subpath = folder_buf[0 .. "../".len + folder.len :0]; - this.package_install.cache_dir = std.fs.cwd(); - }, - else => return, - } - - const needs_install = ctx.skip_verify_installed_version_number or !this.package_install.verify(); - - if (needs_install) { - this.result = this.package_install.install(ctx.skip_verify_installed_version_number); - } else { - this.result = .{ .skip = .{} }; - } - - ctx.channel.writeItem(this) catch unreachable; - } - }; - pub const Summary = struct { fail: u32 = 0, success: u32 = 0, @@ -832,7 +749,7 @@ const PackageInstall = struct { initializeStore(); - package_json_checker = json_parser.PackageJSONVersionChecker.init(allocator, &source, &log) catch return false; + var package_json_checker = json_parser.PackageJSONVersionChecker.init(allocator, &source, &log) catch return false; _ = package_json_checker.parseExpr() catch return false; if (!package_json_checker.has_found_name or !package_json_checker.has_found_version or log.errors > 0) return false; @@ -1458,6 +1375,7 @@ pub const PackageManager = struct { resolve_tasks: TaskChannel, timestamp_for_manifest_cache_control: u32 = 0, extracted_count: u32 = 0, + alias_map: std.ArrayHashMapUnmanaged(PackageID, String, ArrayIdentityContext, false) = .{}, default_features: Features = Features{}, summary: Lockfile.Package.Diff.Summary = Lockfile.Package.Diff.Summary{}, env: *DotEnv.Loader, @@ -2060,7 +1978,7 @@ pub const PackageManager = struct { Semver.Version.sortGt, ); for (installed_versions.items) |installed_version| { - if (version.value.npm.satisfies(installed_version)) { + if (version.value.npm.version.satisfies(installed_version)) { var buf: [bun.MAX_PATH_BYTES]u8 = undefined; var npm_package_path = this.pathForCachedNPMPath(&buf, package_name, installed_version) catch |err| { Output.debug("error getting path for cached npm path: {s}", .{std.mem.span(@errorName(err))}); @@ -2069,7 +1987,10 @@ pub const PackageManager = struct { const dependency = Dependency.Version{ .tag = .npm, .value = .{ - .npm = Semver.Query.Group.from(installed_version), + .npm = .{ + .name = String.init(package_name, package_name), + .version = Semver.Query.Group.from(installed_version), + }, }, }; switch (FolderResolution.getOrPut(.{ .cache_folder = npm_package_path }, dependency, ".", this)) { @@ -2102,8 +2023,9 @@ pub const PackageManager = struct { network_task: ?*NetworkTask = null, }; - pub fn getOrPutResolvedPackageWithFindResult( + fn getOrPutResolvedPackageWithFindResult( this: *PackageManager, + alias: String, name_hash: PackageNameHash, name: String, version: Dependency.Version, @@ -2135,8 +2057,8 @@ pub const PackageManager = struct { }; } - var package = - try Lockfile.Package.fromNPM( + // appendPackage sets the PackageID on the package + const package = try this.lockfile.appendPackage(try Lockfile.Package.fromNPM( this.allocator, this.lockfile, this.log, @@ -2145,10 +2067,9 @@ pub const PackageManager = struct { find_result.package, manifest.string_buf, Features.npm, - ); + )); - // appendPackage sets the PackageID on the package - package = try this.lockfile.appendPackage(package); + try this.alias_map.put(this.allocator, package.meta.id, alias); if (!behavior.isEnabled(if (this.isRootDependency(dependency_id)) this.options.local_package_features @@ -2173,7 +2094,7 @@ pub const PackageManager = struct { .extract => { const task_id = Task.Id.forNPMPackage( Task.Tag.extract, - name.slice(this.lockfile.buffers.string_bytes.items), + this.lockfile.str(name), package.resolution.value.npm.version, ); @@ -2234,64 +2155,6 @@ pub const PackageManager = struct { return network_task; } - pub fn fetchVersionsForPackageName( - this: *PackageManager, - name: string, - version: Dependency.Version, - id: PackageID, - ) !?Npm.PackageManifest { - const task_id = Task.Id.forManifest(Task.Tag.package_manifest, name); - var network_entry = try this.network_dedupe_map.getOrPutContext(this.allocator, task_id, .{}); - var loaded_manifest: ?Npm.PackageManifest = null; - if (!network_entry.found_existing) { - if (this.options.enable.manifest_cache) { - if (this.manifests.get(std.hash.Wyhash.hash(0, name)) orelse (Npm.PackageManifest.Serializer.load(this.allocator, this.cache_directory, name) catch null)) |manifest_| { - const manifest: Npm.PackageManifest = manifest_; - loaded_manifest = manifest; - - if (this.options.enable.manifest_cache_control and manifest.pkg.public_max_age > this.timestamp_for_manifest_cache_control) { - try this.manifests.put(this.allocator, @truncate(PackageNameHash, manifest.pkg.name.hash), manifest); - } - - // If it's an exact package version already living in the cache - // We can skip the network request, even if it's beyond the caching period - if (version.tag == .npm and version.value.npm.isExact()) { - if (loaded_manifest.?.findByVersion(version.value.npm.head.head.range.left.version) != null) { - return manifest; - } - } - - // Was it recent enough to just load it without the network call? - if (this.options.enable.manifest_cache_control and manifest.pkg.public_max_age > this.timestamp_for_manifest_cache_control) { - return manifest; - } - } - } - - if (PackageManager.verbose_install) { - Output.prettyErrorln("Enqueue package manifest for download: {s}", .{name}); - } - - var network_task = this.getNetworkTask(); - network_task.* = NetworkTask{ - .callback = undefined, - .task_id = task_id, - .allocator = this.allocator, - .package_manager = this, - }; - try network_task.forManifest(name, this.allocator, this.scopeForPackageName(name), loaded_manifest); - this.enqueueNetworkTask(network_task); - } - - var manifest_entry_parse = try this.task_queue.getOrPutContext(this.allocator, task_id, .{}); - if (!manifest_entry_parse.found_existing) { - manifest_entry_parse.value_ptr.* = TaskCallbackList{}; - } - - try manifest_entry_parse.value_ptr.append(this.allocator, TaskCallbackContext{ .request_id = id }); - return null; - } - fn enqueueNetworkTask(this: *PackageManager, task: *NetworkTask) void { if (this.network_task_fifo.writableLength() == 0) { this.flushNetworkQueue(); @@ -2319,8 +2182,9 @@ pub const PackageManager = struct { } } - pub fn getOrPutResolvedPackage( + fn getOrPutResolvedPackage( this: *PackageManager, + alias: String, name_hash: PackageNameHash, name: String, version: Dependency.Version, @@ -2339,7 +2203,7 @@ pub const PackageManager = struct { const manifest = this.manifests.getPtr(name_hash) orelse return null; // manifest might still be downloading. This feels unreliable. const find_result: Npm.PackageManifest.FindResult = switch (version.tag) { .dist_tag => manifest.findByDistTag(this.lockfile.str(version.value.dist_tag)), - .npm => manifest.findBestVersion(version.value.npm), + .npm => manifest.findBestVersion(version.value.npm.version), else => unreachable, } orelse return switch (version.tag) { .npm => error.NoMatchingVersion, @@ -2349,6 +2213,7 @@ pub const PackageManager = struct { return try getOrPutResolvedPackageWithFindResult( this, + alias, name_hash, name, version, @@ -2555,9 +2420,16 @@ pub const PackageManager = struct { comptime successFn: SuccessFn, comptime failFn: ?FailFn, ) !void { - const name = dependency.name; - const name_hash = dependency.name_hash; - const version: Dependency.Version = dependency.version; + const alias = dependency.name; + const name = switch (dependency.version.tag) { + .npm => dependency.version.value.npm.name, + else => alias, + }; + const name_hash = switch (dependency.version.tag) { + .npm => Lockfile.stringHash(this.lockfile.str(name)), + else => dependency.name_hash, + }; + const version = dependency.version; var loaded_manifest: ?Npm.PackageManifest = null; if (comptime !is_main) { @@ -2574,6 +2446,7 @@ pub const PackageManager = struct { .dist_tag, .folder, .npm => { retry_from_manifests_ptr: while (true) { var resolve_result_ = this.getOrPutResolvedPackage( + alias, name_hash, name, version, @@ -2693,9 +2566,10 @@ pub const PackageManager = struct { // If it's an exact package version already living in the cache // We can skip the network request, even if it's beyond the caching period - if (dependency.version.tag == .npm and dependency.version.value.npm.isExact()) { - if (loaded_manifest.?.findByVersion(dependency.version.value.npm.head.head.range.left.version)) |find_result| { + if (dependency.version.tag == .npm and dependency.version.value.npm.version.isExact()) { + if (loaded_manifest.?.findByVersion(dependency.version.value.npm.version.head.head.range.left.version)) |find_result| { if (this.getOrPutResolvedPackageWithFindResult( + alias, name_hash, name, version, @@ -2721,7 +2595,7 @@ pub const PackageManager = struct { } if (PackageManager.verbose_install) { - Output.prettyErrorln("Enqueue package manifest for download: {s}", .{this.lockfile.str(name)}); + Output.prettyErrorln("Enqueue package manifest for download: {s}", .{name_str}); } var network_task = this.getNetworkTask(); @@ -2732,9 +2606,9 @@ pub const PackageManager = struct { .allocator = this.allocator, }; try network_task.forManifest( - this.lockfile.str(name), + name_str, this.allocator, - this.scopeForPackageName(this.lockfile.str(name)), + this.scopeForPackageName(name_str), loaded_manifest, ); this.enqueueNetworkTask(network_task); @@ -2757,6 +2631,7 @@ pub const PackageManager = struct { }, .symlink, .workspace => { const _result = this.getOrPutResolvedPackage( + alias, name_hash, name, version, @@ -5120,7 +4995,7 @@ pub const PackageManager = struct { request.missing_version = true; } else { const sliced = SlicedString.init(request.version_buf, request.version_buf); - request.version = Dependency.parse(allocator, request.version_buf, &sliced, log) orelse Dependency.Version{}; + request.version = Dependency.parse(allocator, String.init(request.name, request.name), request.version_buf, &sliced, log) orelse Dependency.Version{}; } update_requests.append(request) catch break; @@ -5287,7 +5162,7 @@ pub const PackageManager = struct { ); } - pub fn updatePackageJSONAndInstallWithManagerWithUpdates( + fn updatePackageJSONAndInstallWithManagerWithUpdates( ctx: Command.Context, manager: *PackageManager, updates: []UpdateRequest, @@ -5654,11 +5529,12 @@ pub const PackageManager = struct { name: string, resolution: Resolution, ) void { - std.mem.copy(u8, &this.destination_dir_subpath_buf, name); - this.destination_dir_subpath_buf[name.len] = 0; - var destination_dir_subpath: [:0]u8 = this.destination_dir_subpath_buf[0..name.len :0]; - var resolution_buf: [512]u8 = undefined; const buf = this.lockfile.buffers.string_bytes.items; + const alias = if (this.manager.alias_map.get(package_id)) |str| str.slice(buf) else name; + std.mem.copy(u8, &this.destination_dir_subpath_buf, alias); + this.destination_dir_subpath_buf[alias.len] = 0; + var destination_dir_subpath: [:0]u8 = this.destination_dir_subpath_buf[0..alias.len :0]; + var resolution_buf: [512]u8 = undefined; const extern_string_buf = this.lockfile.buffers.extern_strings.items; var resolution_label = std.fmt.bufPrint(&resolution_buf, "{}", .{resolution.fmt(buf)}) catch unreachable; var installer = PackageInstall{ diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index 8fc900649..465398fc6 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -577,6 +577,7 @@ fn preprocessUpdateRequests(old: *Lockfile, updates: []PackageManager.UpdateRequ ); dep.version = Dependency.parse( old.allocator, + dep.name, sliced.slice, &sliced, null, @@ -1523,7 +1524,7 @@ pub fn getPackageID( switch (version_.tag) { .npm => { // is it a peerDependency satisfied by a parent package? - if (version_.value.npm.satisfies(resolutions[id].value.npm.version)) { + if (version_.value.npm.version.satisfies(resolutions[id].value.npm.version)) { return id; } }, @@ -1547,7 +1548,7 @@ pub fn getPackageID( return id; } - if (can_satisfy and version.?.value.npm.satisfies(resolutions[id].value.npm.version)) { + if (can_satisfy and version.?.value.npm.version.satisfies(resolutions[id].value.npm.version)) { return id; } } @@ -1968,9 +1969,7 @@ pub const Package = extern struct { } pub fn fromPackageJSON( - allocator: std.mem.Allocator, lockfile: *Lockfile, - log: *logger.Log, package_json: *PackageJSON, comptime features: Features, ) !Lockfile.Package { @@ -2013,39 +2012,12 @@ pub const Package = extern struct { const package_name: ExternalString = string_builder.append(ExternalString, package_json.name); package.name_hash = package_name.hash; package.name = package_name.value; - var package_version = string_builder.append(String, package_json.version); - var buf = string_builder.allocatedSlice(); - - const version: Dependency.Version = brk: { - if (package_json.version.len > 0) { - const sliced = package_version.sliced(buf); - const name = package.name.slice(buf); - if (Dependency.parse(allocator, name, &sliced, log)) |dep| { - break :brk dep; - } - } - break :brk Dependency.Version{}; + package.resolution = .{ + .tag = .root, + .value = .{ .root = {} }, }; - if (version.tag == .npm and version.value.npm.isExact()) { - package.resolution = Resolution{ - .value = .{ - .npm = .{ - .version = version.value.npm.toVersion(), - .url = .{}, - }, - }, - .tag = .npm, - }; - } else { - package.resolution = Resolution{ - .value = .{ - .root = {}, - }, - .tag = .root, - }; - } const total_len = dependencies_list.items.len + total_dependencies_count; std.debug.assert(dependencies_list.items.len == resolutions_list.items.len); @@ -2235,6 +2207,7 @@ pub const Package = extern struct { group.behavior, .version = Dependency.parse( allocator, + name.value, sliced.slice, &sliced, log, @@ -2440,6 +2413,7 @@ pub const Package = extern struct { var dependency_version = Dependency.parseWithOptionalTag( allocator, + external_name.value, sliced.slice, tag, &sliced, @@ -3539,7 +3513,7 @@ pub fn resolve(this: *Lockfile, package_name: []const u8, version: Dependency.Ve .PackageID => |id| { const resolutions = this.packages.items(.resolution); - if (can_satisfy and version.value.npm.satisfies(resolutions[id].value.npm.version)) { + if (can_satisfy and version.value.npm.version.satisfies(resolutions[id].value.npm.version)) { return id; } }, @@ -3554,7 +3528,7 @@ pub fn resolve(this: *Lockfile, package_name: []const u8, version: Dependency.Ve if (id == invalid_package_id - 1) return null; - if (can_satisfy and version.value.npm.satisfies(resolutions[id].value.npm.version)) { + if (can_satisfy and version.value.npm.version.satisfies(resolutions[id].value.npm.version)) { return id; } } diff --git a/src/install/resolvers/folder_resolver.zig b/src/install/resolvers/folder_resolver.zig index 4d0391b64..7db777d85 100644 --- a/src/install/resolvers/folder_resolver.zig +++ b/src/install/resolvers/folder_resolver.zig @@ -216,7 +216,7 @@ pub const FolderResolution = union(Tag) { version, Features.npm, CacheFolderResolver, - CacheFolderResolver{ .version = version.value.npm.toVersion() }, + CacheFolderResolver{ .version = version.value.npm.version.toVersion() }, ), } catch |err| { if (err == error.FileNotFound) { diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig index 8a787c854..9a0aea6a2 100644 --- a/src/resolver/package_json.zig +++ b/src/resolver/package_json.zig @@ -760,8 +760,8 @@ pub const PackageJSON = struct { if (tag == .npm) { const sliced = Semver.SlicedString.init(package_json.version, package_json.version); - if (Dependency.parseWithTag(r.allocator, package_json.version, .npm, &sliced, r.log)) |dependency_version| { - if (dependency_version.value.npm.isExact()) { + if (Dependency.parseWithTag(r.allocator, String.init(package_json.name, package_json.name), package_json.version, .npm, &sliced, r.log)) |dependency_version| { + if (dependency_version.value.npm.version.isExact()) { if (pm.lockfile.resolve(package_json.name, dependency_version)) |resolved| { package_json.package_manager_package_id = resolved; if (resolved > 0) { @@ -870,20 +870,22 @@ pub const PackageJSON = struct { if (group_json.data == .e_object) { var group_obj = group_json.data.e_object; for (group_obj.properties.slice()) |*prop| { - const name = prop.key orelse continue; - const name_str = name.asString(r.allocator) orelse continue; + const name_prop = prop.key orelse continue; + const name_str = name_prop.asString(r.allocator) orelse continue; + const name = String.init(name_str, name_str); const version_value = prop.value orelse continue; const version_str = version_value.asString(r.allocator) orelse continue; const sliced_str = Semver.SlicedString.init(version_str, version_str); if (Dependency.parse( r.allocator, + name, version_str, &sliced_str, r.log, )) |dependency_version| { const dependency = Dependency{ - .name = String.init(name_str, name_str), + .name = name, .version = dependency_version, .name_hash = bun.hash(name_str), .behavior = group.behavior, diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index f9ccf014d..540e61f0b 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -1578,6 +1578,7 @@ pub const Resolver = struct { } dependency_version = Dependency.parse( r.allocator, + Semver.String.init(esm.name, esm.name), esm.version, &sliced_string, r.log, @@ -1829,9 +1830,7 @@ pub const Resolver = struct { if (is_main) { if (package_json_) |package_json| { package = Package.fromPackageJSON( - pm.allocator, pm.lockfile, - r.log, package_json, Install.Features{ .dev_dependencies = true, @@ -1842,12 +1841,6 @@ pub const Resolver = struct { ) catch |err| { return .{ .failure = err }; }; - - package.resolution = .{ - .tag = .root, - .value = .{ .root = {} }, - }; - package = pm.lockfile.appendPackage(package) catch |err| { return .{ .failure = err }; }; diff --git a/test/bun.js/install/bun-install.test.ts b/test/bun.js/install/bun-install.test.ts index 8de41fbc8..980c5ca18 100644 --- a/test/bun.js/install/bun-install.test.ts +++ b/test/bun.js/install/bun-install.test.ts @@ -15,6 +15,12 @@ import { bunEnv } from "bunEnv"; let handler, package_dir, requested, server; +async function readdirSorted(path: PathLike): Promise<string[]> { + const results = await readdir(path); + results.sort(); + return results; +} + function resetHanlder() { handler = function () { return new Response("Tea Break~", { status: 418 }); @@ -78,7 +84,9 @@ it("should handle missing package", async () => { ); expect(stdout).toBeDefined(); expect(await new Response(stdout).text()).toBe(""); - expect(urls).toContain("http://localhost:54321/foo"); + expect(urls).toEqual([ + "http://localhost:54321/foo", + ]); expect(await exited).toBe(1); expect(requested).toBe(1); }); @@ -122,7 +130,9 @@ it("should handle @scoped authentication", async () => { expect(err.split(/\r?\n/)).toContain(`GET ${url} - 555`); expect(stdout).toBeDefined(); expect(await new Response(stdout).text()).toBe(""); - expect(urls).toContain(url); + expect(urls).toEqual([ + url, + ]); expect(seen_token).toBe(true); expect(await exited).toBe(1); expect(requested).toBe(1); @@ -353,7 +363,7 @@ it("should handle inter-dependency between workspaces (optionalDependencies)", a ]); expect(await exited).toBe(0); expect(requested).toBe(0); - expect(await await readdirSorted(join(package_dir, "node_modules"))).toEqual([ + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ "Bar", "Baz", ]); @@ -477,10 +487,134 @@ it("should handle life-cycle scripts within workspaces", async () => { ); }); -var readdirSorted = async ( - ...args: Parameters<typeof readdir> -): ReturnType<typeof readdir> => { - const results = await readdir(...args); - results.sort(); - return results; -}; +it("should handle dependency aliasing", async () => { + const urls: string[] = []; + handler = async (request) => { + expect(request.method).toBe("GET"); + expect(request.headers.get("accept")).toBe( + "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*", + ); + expect(request.headers.get("npm-auth-type")).toBe(null); + expect(await request.text()).toBe(""); + urls.push(request.url); + return new Response("not to be found", { status: 404 }); + }; + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + "Bar": "npm:baz", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toContain('error: package "baz" not found localhost/baz 404'); + expect(err).toContain("error: Bar@npm:baz failed to resolve"); + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([""]); + expect(urls).toEqual([ + "http://localhost:54321/baz", + ]); + expect(await exited).toBe(1); + expect(requested).toBe(1); +}); + +it("should handle dependency aliasing (versioned)", async () => { + const urls: string[] = []; + handler = async (request) => { + expect(request.method).toBe("GET"); + expect(request.headers.get("accept")).toBe( + "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*", + ); + expect(request.headers.get("npm-auth-type")).toBe(null); + expect(await request.text()).toBe(""); + urls.push(request.url); + return new Response("not to be found", { status: 404 }); + }; + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + "Bar": "npm:baz@0.0.2", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toContain('error: package "baz" not found localhost/baz 404'); + expect(err).toContain("error: Bar@npm:baz@0.0.2 failed to resolve"); + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([""]); + expect(urls).toEqual([ + "http://localhost:54321/baz", + ]); + expect(await exited).toBe(1); + expect(requested).toBe(1); +}); + +it("should handle dependency aliasing (dist-tagged)", async () => { + const urls: string[] = []; + handler = async (request) => { + expect(request.method).toBe("GET"); + expect(request.headers.get("accept")).toBe( + "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*", + ); + expect(request.headers.get("npm-auth-type")).toBe(null); + expect(await request.text()).toBe(""); + urls.push(request.url); + return new Response("not to be found", { status: 404 }); + }; + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + "Bar": "npm:baz@latest", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toContain('error: package "baz" not found localhost/baz 404'); + expect(err).toContain("error: Bar@npm:baz@latest failed to resolve"); + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([""]); + expect(urls).toEqual([ + "http://localhost:54321/baz", + ]); + expect(await exited).toBe(1); + expect(requested).toBe(1); +}); |