pub fn view(allocator: std.mem.Allocator, manager: *PackageManager, spec_: string, property_path: ?string, json_output: bool) !void { const name, var version = bun.install.Dependency.splitNameAndVersionOrLatest(brk: { // Extremely best effort. if (bun.strings.eqlComptime(spec_, ".") or bun.strings.eqlComptime(spec_, "")) { if (bun.strings.isNPMPackageName(manager.root_package_json_name_at_time_of_init)) { break :brk manager.root_package_json_name_at_time_of_init; } // Try our best to get the package.json name they meant if (manager.root_dir.hasComptimeQuery("package.json")) from_package_json: { if (manager.root_dir.fd.isValid()) { switch (bun.sys.File.readFrom(manager.root_dir.fd, "package.json", allocator)) { .err => {}, .result => |str| { const source = &logger.Source.initPathString("package.json", str); var log = logger.Log.init(allocator); const json = JSON.parse(source, &log, allocator, false) catch break :from_package_json; if (json.getStringCloned(allocator, "name") catch null) |name| { if (name.len > 0) { break :brk name; } } }, } } } break :brk std.fs.path.basename(bun.fs.FileSystem.instance.top_level_dir); } break :brk spec_; }); const scope = manager.scopeForPackageName(name); var url_buf: bun.PathBuffer = undefined; const encoded_name = try std.fmt.bufPrint(&url_buf, "{s}", .{bun.fmt.dependencyUrl(name)}); var path_buf: bun.PathBuffer = undefined; // Always fetch the full registry manifest, not a specific version const url = URL.parse(try std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ strings.withoutTrailingSlash(scope.url.href), encoded_name, })); var headers: http.HeaderBuilder = .{}; headers.count("Accept", "application/json"); if (scope.token.len > 0) { headers.count("Authorization", ""); headers.content.cap += "Bearer ".len + scope.token.len; } else if (scope.auth.len > 0) { headers.count("Authorization", ""); headers.content.cap += "Basic ".len + scope.auth.len; } try headers.allocate(allocator); headers.append("Accept", "application/json"); if (scope.token.len > 0) { headers.appendFmt("Authorization", "Bearer {s}", .{scope.token}); } else if (scope.auth.len > 0) { headers.appendFmt("Authorization", "Basic {s}", .{scope.auth}); } var response_buf = try MutableString.init(allocator, 2048); var req = http.AsyncHTTP.initSync( allocator, .GET, url, headers.entries, headers.content.ptr.?[0..headers.content.len], &response_buf, "", manager.httpProxy(url), null, .follow, ); req.client.flags.reject_unauthorized = manager.tlsRejectUnauthorized(); const res = req.sendSync() catch |err| { Output.err(err, "view request failed to send", .{}); Global.crash(); }; if (res.status_code >= 400) { try @import("../install/npm.zig").responseError(allocator, &req, &res, .{ name, version }, &response_buf, false); } var log = logger.Log.init(allocator); const source = &logger.Source.initPathString("view.json", response_buf.list.items); var json = JSON.parseUTF8(source, &log, allocator) catch |err| { Output.err(err, "failed to parse response body as JSON", .{}); Global.crash(); }; if (log.errors > 0) { try log.print(Output.errorWriter()); Global.crash(); } // Parse the existing JSON response into a PackageManifest using the now-public parse function const parsed_manifest = @import("../install/npm.zig").PackageManifest.parse( allocator, scope, &log, response_buf.list.items, name, "", // last_modified (not needed for view) "", // etag (not needed for view) 0, // public_max_age (not needed for view) true, // is_extended_manifest (view uses application/json Accept header) ) catch |err| { Output.err(err, "failed to parse package manifest", .{}); Global.exit(1); } orelse { Output.errGeneric("failed to parse package manifest", .{}); Global.crash(); }; // Now use the existing version resolution logic from outdated_command var manifest = json; var versions_len: usize = 1; version, manifest = brk: { if (json.getObject("versions")) |versions_obj| from_versions: { // Find the version string from JSON that matches the resolved version const versions = versions_obj.data.e_object.properties.slice(); versions_len = versions.len; const wanted_version: Semver.Version = brk2: { // First try dist-tag lookup (like "latest", "beta", etc.) if (parsed_manifest.findByDistTag(version)) |result| { break :brk2 result.version; } else { // Parse as semver query and find best version - exactly like outdated_command.zig line 325 const sliced_literal = Semver.SlicedString.init(version, version); const query = try Semver.Query.parse(allocator, version, sliced_literal); defer query.deinit(); // Use the same pattern as outdated_command: findBestVersion(query.head, string_buf) if (parsed_manifest.findBestVersion(query, parsed_manifest.string_buf)) |result| { break :brk2 result.version; } } break :from_versions; }; for (versions) |*prop| { if (prop.key == null) continue; const version_str = prop.key.?.asString(allocator) orelse continue; const sliced_version = Semver.SlicedString.init(version_str, version_str); const parsed_version = Semver.Version.parse(sliced_version); if (parsed_version.valid and parsed_version.version.max().eql(wanted_version)) { break :brk .{ version_str, prop.value.? }; } } } if (json_output) { Output.print("{{ \"error\": \"No matching version found\", \"version\": {} }}\n", .{ bun.fmt.formatJSONStringUTF8(spec_, .{ .quote = true, }), }); Output.flush(); } else { Output.errGeneric("No version of {} satisfying {} found", .{ bun.fmt.quote(name), bun.fmt.quote(version), }); const max_versions_to_display = 5; const start_index = parsed_manifest.versions.len -| max_versions_to_display; var versions_to_display = parsed_manifest.versions[start_index..]; versions_to_display = versions_to_display[0..@min(versions_to_display.len, max_versions_to_display)]; if (versions_to_display.len > 0) { Output.prettyErrorln("\nRecent versions:", .{}); for (versions_to_display) |*v| { Output.prettyErrorln("- {}", .{v.fmt(parsed_manifest.string_buf)}); } if (start_index > 0) { Output.prettyErrorln(" ... and {d} more", .{start_index}); } } } Global.exit(1); }; // Treat versions specially because npm does some normalization on there. if (json.getObject("versions")) |versions_object| { const keys = try allocator.alloc(bun.ast.Expr, versions_object.data.e_object.properties.len); for (versions_object.data.e_object.properties.slice(), keys) |*prop, *key| { key.* = prop.key.?; } const versions_array = bun.ast.Expr.init( bun.ast.E.Array, bun.ast.E.Array{ .items = .fromOwnedSlice(keys), }, .{ .start = -1 }, ); try manifest.set(allocator, "versions", versions_array); } // Handle property lookup if specified if (property_path) |prop_path| { // This is similar to what npm does. // `bun pm view react version ` => 1.2.3 // `bun pm view react versions` => ['1.2.3', '1.2.4', '1.2.5'] if (manifest.getPathMayBeIndex(prop_path) orelse json.getPathMayBeIndex(prop_path)) |value| { if (value.data == .e_string) { const slice = value.data.e_string.slice(allocator); if (json_output) { Output.println("{s}", .{bun.fmt.formatJSONStringUTF8(slice, .{})}); } else { Output.println("{s}", .{slice}); } Output.flush(); return; } const JSPrinter = bun.js_printer; var buffer_writer = JSPrinter.BufferWriter.init(bun.default_allocator); buffer_writer.append_newline = true; var package_json_writer = JSPrinter.BufferPrinter.init(buffer_writer); _ = try bun.js_printer.printJSON( @TypeOf(&package_json_writer), &package_json_writer, value, source, .{ .mangled_props = null, }, ); Output.print("{s}", .{package_json_writer.ctx.getWritten()}); Output.flush(); Global.exit(0); } else { if (json_output) { Output.print("{{ \"error\": \"Property not found\", \"version\": {}, \"property\": {} }}\n", .{ bun.fmt.formatJSONStringUTF8(spec_, .{ .quote = true, }), bun.fmt.formatJSONStringUTF8(prop_path, .{ .quote = true, }), }); Output.flush(); } else { Output.errGeneric("Property {s} not found", .{prop_path}); } } Global.exit(1); } if (json_output) { // Output formatted JSON using JSPrinter const JSPrinter = bun.js_printer; var buffer_writer = JSPrinter.BufferWriter.init(bun.default_allocator); buffer_writer.append_newline = true; var package_json_writer = JSPrinter.BufferPrinter.init(buffer_writer); _ = try bun.js_printer.printJSON( @TypeOf(&package_json_writer), &package_json_writer, manifest, source, .{ .mangled_props = null, .indent = .{ .count = 2, }, }, ); Output.print("{s}", .{package_json_writer.ctx.getWritten()}); Output.flush(); return; } const pkg_name = manifest.getStringCloned(allocator, "name") catch null orelse name; const pkg_version = manifest.getStringCloned(allocator, "version") catch null orelse version; const license = manifest.getStringCloned(allocator, "license") catch null orelse ""; var dep_count: usize = 0; const dependencies_object = manifest.getObject("dependencies"); if (dependencies_object) |*deps| { dep_count = deps.data.e_object.properties.len; } Output.prettyln("{s}@{s} | {s} | deps: {d} | versions: {d}", .{ pkg_name, pkg_version, license, dep_count, versions_len, }); // Get description and homepage from the top-level package manifest, not the version-specific one if (json.getStringCloned(allocator, "description") catch null) |desc| { Output.prettyln("{s}", .{desc}); } if (json.getStringCloned(allocator, "homepage") catch null) |hp| { Output.prettyln("{s}", .{hp}); } if (json.getArray("keywords")) |arr| { var keywords = try MutableString.init(allocator, 64); var iter = arr; var first = true; while (iter.next()) |kw_expr| { if (kw_expr.asString(allocator)) |kw| { if (!first) try keywords.appendSlice(", ") else first = false; try keywords.appendSlice(kw); } } if (keywords.list.items.len > 0) { Output.prettyln("keywords: {s}", .{keywords.list.items}); } } // Display dependencies if they exist if (dependencies_object) |*deps| { const dependencies = deps.data.e_object.properties.slice(); if (dependencies.len > 0) { Output.prettyln("\ndependencies ({d}):", .{dependencies.len}); } for (dependencies) |prop| { if (prop.key == null or prop.value == null) continue; const dep_name = prop.key.?.asString(allocator) orelse continue; const dep_version = prop.value.?.asString(allocator) orelse continue; Output.prettyln("- {s}: {s}", .{ dep_name, dep_version }); } } if (manifest.getObject("dist")) |dist| { Output.prettyln("\ndist", .{}); if (dist.getStringCloned(allocator, "tarball") catch null) |t| { Output.prettyln(" .tarball: {s}", .{t}); } if (dist.getStringCloned(allocator, "shasum") catch null) |s| { Output.prettyln(" .shasum: {s}", .{s}); } if (dist.getStringCloned(allocator, "integrity") catch null) |i| { Output.prettyln(" .integrity: {s}", .{i}); } if (dist.getNumber("unpackedSize")) |u| { Output.prettyln(" .unpackedSize: {}", .{bun.fmt.size(@as(u64, @intFromFloat(u[0])), .{})}); } } if (json.getObject("dist-tags")) |tags_obj| { Output.prettyln("\ndist-tags:", .{}); for (tags_obj.data.e_object.properties.slice()) |prop| { if (prop.key == null or prop.value == null) continue; const tagname_expr = prop.key.?; const val_expr = prop.value.?; if (tagname_expr.asString(allocator)) |tag| { if (val_expr.asString(allocator)) |val| { if (strings.eqlComptime(tag, "latest")) { Output.prettyln("{s}: {s}", .{ tag, val }); } else if (strings.eqlComptime(tag, "beta")) { Output.prettyln("{s}: {s}", .{ tag, val }); } else { Output.prettyln("{s}: {s}", .{ tag, val }); } } } } } if (json.getArray("maintainers")) |maint_iter| { Output.prettyln("\nmaintainers:", .{}); var iter = maint_iter; while (iter.next()) |m| { const nm = m.getStringCloned(allocator, "name") catch null orelse ""; const em = m.getStringCloned(allocator, "email") catch null orelse ""; if (em.len > 0) { Output.prettyln("- {s} \\<{s}\\>", .{ nm, em }); } else if (nm.len > 0) { Output.prettyln("- {s}", .{nm}); } } } // Add published date information if (json.getObject("time")) |time_obj| { // TODO: use a relative time formatter if (time_obj.getStringCloned(allocator, pkg_version) catch null) |published_time| { Output.prettyln("\nPublished: {s}", .{published_time}); } else if (time_obj.getStringCloned(allocator, "modified") catch null) |modified_time| { Output.prettyln("\nPublished: {s}", .{modified_time}); } } } const string = []const u8; const std = @import("std"); const PackageManager = @import("../install/install.zig").PackageManager; const PackageManifest = @import("../install/npm.zig").PackageManifest; const URL = @import("../url.zig").URL; const bun = @import("bun"); const Global = bun.Global; const JSON = bun.json; const MutableString = bun.MutableString; const Output = bun.Output; const Semver = bun.Semver; const http = bun.http; const logger = bun.logger; const strings = bun.strings;