const std = @import("std"); const PackageID = @import("../install.zig").PackageID; const Lockfile = @import("../install.zig").Lockfile; const PackageManager = @import("../install.zig").PackageManager; const Npm = @import("../npm.zig"); const logger = @import("bun").logger; const FileSystem = @import("../../fs.zig").FileSystem; const JSAst = @import("../../js_ast.zig"); const string = @import("../../string_types.zig").string; const Features = @import("../install.zig").Features; const IdentityContext = @import("../../identity_context.zig").IdentityContext; const strings = @import("bun").strings; const Resolution = @import("../resolution.zig").Resolution; const Repository = @import("../repository.zig").Repository; const String = @import("../semver.zig").String; const Semver = @import("../semver.zig"); const bun = @import("bun"); const Dependency = @import("../dependency.zig"); pub const FolderResolution = union(Tag) { package_id: PackageID, new_package_id: PackageID, err: anyerror, pub const Tag = enum { package_id, err, new_package_id }; pub const Map = std.HashMapUnmanaged(u64, FolderResolution, IdentityContext(u64), 80); pub fn normalize(path: string) string { return FileSystem.instance.normalize(path); } pub fn hash(normalized_path: string) u64 { return std.hash.Wyhash.hash(0, normalized_path); } pub fn NewResolver(comptime tag: Resolution.Tag) type { return struct { folder_path: string, pub fn resolve(this: @This(), comptime Builder: type, builder: Builder, _: JSAst.Expr) !Resolution { return Resolution{ .tag = tag, .value = @unionInit(Resolution.Value, @tagName(tag), builder.append(String, this.folder_path)), }; } pub fn count(this: @This(), comptime Builder: type, builder: Builder, _: JSAst.Expr) void { builder.count(this.folder_path); } }; } pub const Resolver = NewResolver(Resolution.Tag.folder); pub const SymlinkResolver = NewResolver(Resolution.Tag.symlink); pub const CacheFolderResolver = struct { folder_path: []const u8 = "", version: Semver.Version, pub fn resolve(this: @This(), comptime Builder: type, _: Builder, _: JSAst.Expr) !Resolution { return Resolution{ .tag = Resolution.Tag.npm, .value = .{ .npm = .{ .version = this.version, .url = String.init("", ""), }, }, }; } pub fn count(_: @This(), comptime Builder: type, _: Builder, _: JSAst.Expr) void {} }; pub const GitCacheFolderResolver = struct { repository: Repository, version_tag: Dependency.Version.Tag, pub fn resolve(this: @This(), comptime Builder: type, _: Builder, _: JSAst.Expr) !Resolution { return Resolution{ .tag = if (this.version_tag == .git) Resolution.Tag.git else Resolution.Tag.github, .value = .{ .git = this.repository, }, }; } pub fn count(_: @This(), comptime Builder: type, _: Builder, _: JSAst.Expr) void {} }; pub fn normalizePackageJSONPath(global_or_relative: GlobalOrRelative, joined: *[bun.MAX_PATH_BYTES]u8, non_normalized_path: string) [2]string { var abs: string = ""; var rel: string = ""; // We consider it valid if there is a package.json in the folder const temp_normalized = std.mem.trimRight(u8, normalize(non_normalized_path), std.fs.path.sep_str); var normalized_buf: [bun.MAX_PATH_BYTES]u8 = undefined; std.mem.copy(u8, &normalized_buf, temp_normalized); var normalized = normalized_buf[0..temp_normalized.len]; if (global_or_relative == .git_cache_folder and normalized[0] != '/') { var temp_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; temp_path_buf[0] = '/'; std.mem.copy(u8, temp_path_buf[1..], normalized); normalized.len += 1; std.mem.copy(u8, normalized, temp_path_buf[0..normalized.len]); } if (strings.startsWithChar(normalized, '.')) { var tempcat: [bun.MAX_PATH_BYTES]u8 = undefined; std.mem.copy(u8, &tempcat, normalized); tempcat[normalized.len] = std.fs.path.sep; std.mem.copy(u8, tempcat[normalized.len + 1 ..], "package.json"); var parts = [_]string{ FileSystem.instance.top_level_dir, tempcat[0 .. normalized.len + 1 + "package.json".len] }; abs = FileSystem.instance.absBuf(&parts, joined); rel = FileSystem.instance.relative(FileSystem.instance.top_level_dir, abs[0 .. abs.len - "/package.json".len]); } else { var remain: []u8 = joined[0..]; switch (global_or_relative) { .global, .cache_folder => { const path = if (global_or_relative == .global) global_or_relative.global else global_or_relative.cache_folder; if (path.len > 0) { const offset = path.len -| @as(usize, @boolToInt(path[path.len -| 1] == std.fs.path.sep)); if (offset > 0) @memcpy(remain.ptr, path.ptr, offset); remain = remain[offset..]; if (normalized.len > 0) { if ((path[path.len - 1] != std.fs.path.sep) and (normalized[0] != std.fs.path.sep)) { remain[0] = std.fs.path.sep; remain = remain[1..]; } } } }, else => {}, } std.mem.copy(u8, remain, normalized); remain[normalized.len] = std.fs.path.sep; remain[normalized.len + 1 ..][0.."package.json".len].* = "package.json".*; remain = remain[normalized.len + 1 + "package.json".len ..]; abs = joined[0 .. joined.len - remain.len]; // We store the folder name without package.json rel = abs[0 .. abs.len - "/package.json".len]; } return .{ abs, rel }; } pub fn readPackageJSONFromBytes( manager: *PackageManager, abs: []const u8, json_bytes: string, version: Dependency.Version, comptime features: Features, comptime ResolverType: type, resolver: ResolverType, ) !Lockfile.Package { var package = Lockfile.Package{}; var body = Npm.Registry.BodyPool.get(manager.allocator); defer Npm.Registry.BodyPool.release(body); const source = logger.Source.initPathString(abs, json_bytes); try Lockfile.Package.parse( manager.lockfile, &package, manager.allocator, manager.log, source, ResolverType, resolver, features, ); if (manager.lockfile.getPackageID(package.name_hash, version, package.resolution)) |existing_id| { return manager.lockfile.packages.get(existing_id); } return manager.lockfile.appendPackage(package) catch unreachable; } pub fn readPackageJSONFromDisk( manager: *PackageManager, joinedZ: [:0]const u8, abs: []const u8, version: Dependency.Version, comptime features: Features, comptime ResolverType: type, resolver: ResolverType, ) !Lockfile.Package { var package_json: std.fs.File = try std.fs.cwd().openFileZ(joinedZ, .{ .mode = .read_only }); defer package_json.close(); var package = Lockfile.Package{}; var body = Npm.Registry.BodyPool.get(manager.allocator); defer Npm.Registry.BodyPool.release(body); const len = try package_json.getEndPos(); body.data.reset(); body.data.inflate(@max(len, 2048)) catch unreachable; body.data.list.expandToCapacity(); const source_buf = try package_json.readAll(body.data.list.items); const source = logger.Source.initPathString(abs, body.data.list.items[0..source_buf]); try Lockfile.Package.parse( manager.lockfile, &package, manager.allocator, manager.log, source, ResolverType, resolver, features, ); if (manager.lockfile.getPackageID(package.name_hash, version, package.resolution)) |existing_id| { return manager.lockfile.packages.get(existing_id); } return manager.lockfile.appendPackage(package) catch unreachable; } pub const GlobalOrRelative = union(enum) { global: []const u8, relative: void, cache_folder: []const u8, git_cache_folder: []const u8, }; pub fn getOrPutWithPackageJSONBytes(global_or_relative: GlobalOrRelative, version: Dependency.Version, non_normalized_path: string, manager: *PackageManager, json_bytes: string) FolderResolution { var joined: [bun.MAX_PATH_BYTES]u8 = undefined; const paths = normalizePackageJSONPath(global_or_relative, &joined, non_normalized_path); const abs = paths[0]; const rel = paths[1]; var entry = manager.folders.getOrPut(manager.allocator, hash(abs)) catch unreachable; if (entry.found_existing) { if (global_or_relative != .git_cache_folder or entry.value_ptr.*.err != error.MissingPackageJSON) { return entry.value_ptr.*; } } const package: Lockfile.Package = switch (global_or_relative) { .global => readPackageJSONFromBytes( manager, abs, json_bytes, version, Features.link, SymlinkResolver, SymlinkResolver{ .folder_path = non_normalized_path }, ), .relative => readPackageJSONFromBytes( manager, abs, json_bytes, version, Features.folder, Resolver, Resolver{ .folder_path = rel }, ), .cache_folder => readPackageJSONFromBytes( manager, abs, json_bytes, version, Features.npm, CacheFolderResolver, CacheFolderResolver{ .version = version.value.npm.toVersion() }, ), .git_cache_folder => readPackageJSONFromBytes( manager, abs, json_bytes, version, Features.git, GitCacheFolderResolver, GitCacheFolderResolver{ .repository = if (version.tag == .git) version.value.git else version.value.github, .version_tag = version.tag, }, ), } catch |err| { if (err == error.FileNotFound) { entry.value_ptr.* = .{ .err = error.MissingPackageJSON }; } else { entry.value_ptr.* = .{ .err = err }; } return entry.value_ptr.*; }; entry.value_ptr.* = .{ .package_id = package.meta.id }; return FolderResolution{ .new_package_id = package.meta.id }; } pub fn getOrPut(global_or_relative: GlobalOrRelative, version: Dependency.Version, non_normalized_path: string, manager: *PackageManager) FolderResolution { var joined: [bun.MAX_PATH_BYTES]u8 = undefined; const paths = normalizePackageJSONPath(global_or_relative, &joined, non_normalized_path); const abs = paths[0]; const rel = paths[1]; var entry = manager.folders.getOrPut(manager.allocator, hash(abs)) catch unreachable; if (entry.found_existing) { if (global_or_relative != .git_cache_folder or entry.value_ptr.*.err != error.MissingPackageJSON) { return entry.value_ptr.*; } } joined[abs.len] = 0; var joinedZ: [:0]u8 = joined[0..abs.len :0]; const package: Lockfile.Package = switch (global_or_relative) { .global => readPackageJSONFromDisk( manager, joinedZ, abs, version, Features.link, SymlinkResolver, SymlinkResolver{ .folder_path = non_normalized_path }, ), .relative => readPackageJSONFromDisk( manager, joinedZ, abs, version, Features.folder, Resolver, Resolver{ .folder_path = rel }, ), .cache_folder => readPackageJSONFromDisk( manager, joinedZ, abs, version, Features.npm, CacheFolderResolver, CacheFolderResolver{ .version = version.value.npm.toVersion() }, ), .git_cache_folder => readPackageJSONFromDisk( manager, joinedZ, abs, version, Features.git, GitCacheFolderResolver, GitCacheFolderResolver{ .repository = if (version.tag == .git) version.value.git else version.value.github, .version_tag = version.tag, }, ), } catch |err| { if (err == error.FileNotFound) { entry.value_ptr.* = .{ .err = error.MissingPackageJSON }; } else { entry.value_ptr.* = .{ .err = err }; } return entry.value_ptr.*; }; entry.value_ptr.* = .{ .package_id = package.meta.id }; return FolderResolution{ .new_package_id = package.meta.id }; } };