aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-12-19 14:38:32 -0800
committerGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-12-19 14:38:32 -0800
commit0c7b35d0aba2e0bde23946063bd15817a7ca9d4d (patch)
tree1823d777589f4fb19f376ec507583da0e8bc150d /src
parente12f10ae0ee1a7f3ac266a64a53267c233bbf089 (diff)
downloadbun-0c7b35d0aba2e0bde23946063bd15817a7ca9d4d.tar.gz
bun-0c7b35d0aba2e0bde23946063bd15817a7ca9d4d.tar.zst
bun-0c7b35d0aba2e0bde23946063bd15817a7ca9d4d.zip
[bun install] Support `file:` protocol for installing folders as packages
Diffstat (limited to 'src')
-rw-r--r--src/install/dependency.zig30
-rw-r--r--src/install/install.zig241
-rw-r--r--src/install/resolution.zig3
3 files changed, 232 insertions, 42 deletions
diff --git a/src/install/dependency.zig b/src/install/dependency.zig
index 478b6bce6..9e0e64736 100644
--- a/src/install/dependency.zig
+++ b/src/install/dependency.zig
@@ -245,7 +245,19 @@ pub const Version = struct {
pub fn infer(dependency: string) Tag {
switch (dependency[0]) {
// npm package
- '=', '>', '<', '0'...'9', '^', '*', '~', '|' => return Tag.npm,
+ '=', '>', '<', '0'...'9', '^', '*', '|' => return Tag.npm,
+
+ '.' => return Tag.folder,
+
+ '~' => {
+
+ // https://docs.npmjs.com/cli/v8/configuring-npm/package-json#local-paths
+ if (dependency.len > 1 and dependency[1] == '/') {
+ return Tag.folder;
+ }
+
+ return Tag.npm;
+ },
'n' => {
if (dependency.len > 4 and strings.eqlComptimeIgnoreLen(dependency[0..4], "npm:")) {
@@ -339,14 +351,14 @@ pub const Version = struct {
return .dist_tag;
},
- // file://
+ // file:
'f' => {
if (isTarball(dependency))
return .tarball;
if (strings.eqlComptime(
- dependency[0..@minimum("file://".len, dependency.len)],
- "file://",
+ dependency[0..@minimum("file:".len, dependency.len)],
+ "file:",
)) {
return .folder;
}
@@ -364,8 +376,8 @@ pub const Version = struct {
return .tarball;
if (strings.eqlComptime(
- dependency[0..@minimum("link://".len, dependency.len)],
- "link://",
+ dependency[0..@minimum("link:".len, dependency.len)],
+ "link:",
)) {
return .symlink;
}
@@ -497,13 +509,13 @@ pub fn parseWithTag(
};
},
.tarball => {
- if (strings.contains(dependency, "://")) {
- if (strings.startsWith(dependency, "file://")) {
+ if (strings.indexOf(dependency, "://")) |protocol| {
+ if (strings.eqlComptime(dependency[0..protocol], "file")) {
return Version{
.tag = .tarball,
.value = .{ .tarball = URI{ .local = sliced.sub(dependency[7..]).value() } },
};
- } else if (strings.startsWith(dependency, "https://") or strings.startsWith(dependency, "http://")) {
+ } else if (strings.eqlComptime(dependency[0..protocol], "http") or strings.eqlComptime(dependency[0..protocol], "https")) {
return Version{
.tag = .tarball,
.value = .{ .tarball = URI{ .remote = sliced.sub(dependency).value() } },
diff --git a/src/install/install.zig b/src/install/install.zig
index ef9872d3e..e046d506f 100644
--- a/src/install/install.zig
+++ b/src/install/install.zig
@@ -344,9 +344,20 @@ pub const Features = struct {
scripts: bool = false,
peer_dependencies: bool = true,
is_main: bool = false,
+ dependencies: bool = true,
check_for_duplicate_dependencies: bool = false,
+ // When it's a folder, we do not parse any of the dependencies
+ pub const folder = Features{
+ .optional_dependencies = false,
+ .dev_dependencies = false,
+ .scripts = false,
+ .peer_dependencies = false,
+ .is_main = false,
+ .dependencies = false,
+ };
+
pub const npm = Features{
.optional_dependencies = true,
};
@@ -1143,6 +1154,8 @@ pub const Lockfile = struct {
visited.set(0);
const end = @truncate(PackageID, names.len);
+ var any_failed = false;
+
if (this.successfully_installed) |installed| {
for (resolutions_list[0].get(resolutions_buffer)) |package_id| {
if (package_id > end or !installed.isSet(package_id)) continue;
@@ -1199,10 +1212,14 @@ pub const Lockfile = struct {
failed_dep.version.literal.fmt(string_buf),
},
);
+ // track this so we can log each failure instead of just the first
+ any_failed = true;
continue;
}
}
}
+
+ if (any_failed) std.os.exit(1);
}
};
@@ -1952,16 +1969,17 @@ pub const Lockfile = struct {
const dependency_groups = comptime brk: {
var out_groups: [
- 1 +
+ @as(usize, @boolToInt(features.dependencies)) +
@as(usize, @boolToInt(features.dev_dependencies)) +
@as(usize, @boolToInt(features.optional_dependencies)) +
@as(usize, @boolToInt(features.peer_dependencies))
]DependencyGroup = undefined;
var out_group_i: usize = 0;
- out_groups[out_group_i] = DependencyGroup.dependencies;
- out_group_i += 1;
-
+ if (features.dependencies) {
+ out_groups[out_group_i] = DependencyGroup.dependencies;
+ out_group_i += 1;
+ }
if (features.dev_dependencies) {
out_groups[out_group_i] = DependencyGroup.dev;
out_group_i += 1;
@@ -2235,12 +2253,25 @@ pub const Lockfile = struct {
return hasher.final();
}
+ pub fn parseMain(
+ lockfile: *Lockfile,
+ package: *Lockfile.Package,
+ allocator: *std.mem.Allocator,
+ log: *logger.Log,
+ source: logger.Source,
+ comptime features: Features,
+ ) !void {
+ return try parse(lockfile, package, allocator, log, source, void, void{}, features);
+ }
+
pub fn parse(
lockfile: *Lockfile,
package: *Lockfile.Package,
allocator: *std.mem.Allocator,
log: *logger.Log,
source: logger.Source,
+ comptime ResolverContext: type,
+ resolver: ResolverContext,
comptime features: Features,
) !void {
initializeStore();
@@ -2275,20 +2306,27 @@ pub const Lockfile = struct {
}
}
+ if (comptime ResolverContext != void) {
+ resolver.count(*Lockfile.StringBuilder, &string_builder, json);
+ }
+
const dependency_groups = comptime brk: {
var out_groups: [
- 2 +
+ @as(usize, @boolToInt(features.dependencies)) +
+ @as(usize, @boolToInt(features.dev_dependencies)) +
@as(usize, @boolToInt(features.optional_dependencies)) +
@as(usize, @boolToInt(features.peer_dependencies))
]DependencyGroup = undefined;
var out_group_i: usize = 0;
+ if (features.dependencies) {
+ out_groups[out_group_i] = DependencyGroup.dependencies;
+ out_group_i += 1;
+ }
- out_groups[out_group_i] = DependencyGroup.dependencies;
- out_group_i += 1;
-
- out_groups[out_group_i] = DependencyGroup.dev;
- out_group_i += 1;
-
+ if (features.dev_dependencies) {
+ out_groups[out_group_i] = DependencyGroup.dev;
+ out_group_i += 1;
+ }
if (features.optional_dependencies) {
out_groups[out_group_i] = DependencyGroup.optional;
out_group_i += 1;
@@ -2335,22 +2373,8 @@ pub const Lockfile = struct {
}
if (comptime !features.is_main) {
- if (json.asProperty("version")) |version_q| {
- if (version_q.expr.asString(allocator)) |version_str_| {
- const version_str: String = string_builder.append(String, version_str_);
- const sliced_string: SlicedString = version_str.sliced(string_buf.allocatedSlice());
-
- const semver_version = Semver.Version.parse(sliced_string, allocator);
-
- if (semver_version.valid) {
- package.resolution = .{
- .tag = .npm,
- .value = .{ .npm = semver_version.version },
- };
- } else {
- log.addErrorFmt(null, logger.Loc.Empty, allocator, "invalid version \"{s}\"", .{version_str}) catch unreachable;
- }
- }
+ if (comptime ResolverContext != void) {
+ package.resolution = try resolver.resolve(*Lockfile.StringBuilder, &string_builder, json);
}
} else {
package.resolution = .{
@@ -3652,6 +3676,7 @@ pub const PackageManager = struct {
thread_pool: ThreadPool,
manifests: PackageManifestMap = PackageManifestMap{},
+ folders: FolderResolution.Map = FolderResolution.Map{},
resolved_package_index: PackageIndex = PackageIndex{},
task_queue: TaskDependencyQueue = .{},
@@ -3948,6 +3973,109 @@ pub const PackageManager = struct {
this.network_task_fifo.writeItemAssumeCapacity(task);
}
+ pub const FolderResolution = union(Tag) {
+ package_id: PackageID,
+ err: anyerror,
+
+ pub const Tag = enum { package_id, err };
+
+ 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 const Resolver = struct {
+ folder_path: string,
+
+ pub fn resolve(this: Resolver, comptime Builder: type, builder: Builder, json: JSAst.Expr) !Resolution {
+ return Resolution{
+ .tag = .folder,
+ .value = .{
+ .folder = builder.append(String, this.folder_path),
+ },
+ };
+ }
+
+ pub fn count(this: Resolver, comptime Builder: type, builder: Builder, json: JSAst.Expr) void {
+ builder.count(this.folder_path);
+ }
+ };
+
+ pub fn getOrPut(non_normalized_path: string, manager: *PackageManager) FolderResolution {
+
+ // We consider it valid if there is a package.json in the folder
+ const normalized = std.mem.trimRight(u8, normalize(non_normalized_path), std.fs.path.sep_str);
+ var joined: [std.fs.MAX_PATH_BYTES]u8 = undefined;
+ var rel_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
+ var abs: string = "";
+ var rel: string = "";
+ if (strings.startsWithChar(normalized, '.')) {
+ var tempcat: [std.fs.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 {
+ std.mem.copy(u8, &joined, normalized);
+ joined[normalized.len] = std.fs.path.sep;
+ joined[normalized.len + 1 ..][0.."package.json".len].* = "package.json".*;
+ abs = joined[0 .. normalized.len + 1 + "package.json".len];
+ // We store the folder name without package.json
+ rel = abs[0 .. abs.len - "/package.json".len];
+ }
+
+ var entry = manager.folders.getOrPut(manager.allocator, hash(abs)) catch unreachable;
+ if (entry.found_existing) return entry.value_ptr.*;
+
+ joined[abs.len] = 0;
+ var joinedZ: [:0]u8 = joined[0..abs.len :0];
+
+ var package_json: std.fs.File = std.fs.cwd().openFileZ(joinedZ, .{ .read = true }) catch |err| {
+ entry.value_ptr.* = .{ .err = err };
+ return entry.value_ptr.*;
+ };
+ var package = Lockfile.Package{};
+ var body = Npm.Registry.BodyPool.get(manager.allocator);
+
+ defer Npm.Registry.BodyPool.release(body);
+ const initial_errors_count = manager.log.errors;
+ const len = package_json.getEndPos() catch |err| {
+ entry.value_ptr.* = .{ .err = err };
+ return entry.value_ptr.*;
+ };
+
+ body.data.reset();
+ body.data.inflate(@maximum(len, 2048)) catch unreachable;
+ body.data.list.expandToCapacity();
+ const source_buf = package_json.readAll(body.data.list.items) catch |err| {
+ entry.value_ptr.* = .{ .err = err };
+ return entry.value_ptr.*;
+ };
+ var resolver = Resolver{
+ .folder_path = rel,
+ };
+ const source = logger.Source.initPathString(abs, body.data.list.items[0..source_buf]);
+
+ Lockfile.Package.parse(manager.lockfile, &package, manager.allocator, manager.log, source, Resolver, resolver, Features.folder) catch |err| {
+ // Folders are considered dependency-less
+ entry.value_ptr.* = .{ .err = err };
+ return entry.value_ptr.*;
+ };
+
+ package = manager.lockfile.appendPackage(package) catch unreachable;
+ entry.value_ptr.* = .{ .package_id = package.meta.id };
+ return FolderResolution{ .package_id = package.meta.id };
+ }
+ };
+
pub fn getOrPutResolvedPackage(
this: *PackageManager,
name_hash: PackageNameHash,
@@ -3978,6 +4106,18 @@ pub const PackageManager = struct {
return try getOrPutResolvedPackageWithFindResult(this, name_hash, name, version, dependency_id, behavior, manifest, find_result);
},
+ .folder => {
+ const res = FolderResolution.getOrPut(name.slice(this.lockfile.buffers.string_bytes.items), this);
+
+ switch (res) {
+ .err => |err| return err,
+ .package_id => |package_id| {
+ this.lockfile.buffers.resolutions.items[dependency_id] = package_id;
+ return ResolvedPackageResult{ .package = this.lockfile.packages.get(res.package_id) };
+ },
+ }
+ },
+
else => return null,
}
}
@@ -4100,7 +4240,7 @@ pub const PackageManager = struct {
}
switch (dependency.version.tag) {
- .npm, .dist_tag => {
+ .folder, .npm, .dist_tag => {
retry_from_manifests_ptr: while (true) {
var resolve_result_ = this.getOrPutResolvedPackage(
name_hash,
@@ -4176,7 +4316,7 @@ pub const PackageManager = struct {
this.enqueueNetworkTask(network_task);
}
}
- } else if (!dependency.behavior.isPeer()) {
+ } else if (!dependency.behavior.isPeer() and dependency.version.tag.isNPM()) {
const name_str = this.lockfile.str(name);
const task_id = Task.Id.forManifest(Task.Tag.package_manifest, name_str);
var network_entry = try this.network_dedupe_map.getOrPutContext(this.allocator, task_id, .{});
@@ -6119,6 +6259,41 @@ pub const PackageManager = struct {
}
}
},
+ // TODO: support folder links higher than node_modules folder
+ .folder => {
+ var folder_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
+ std.mem.copy(u8, &folder_buf, "../" ++ std.fs.path.sep_str);
+ std.mem.copy(u8, folder_buf["../".len..], name);
+ folder_buf["../".len + name.len] = 0;
+ var folderZ: [:0]u8 = folder_buf[0 .. "../".len + name.len :0];
+
+ const needs_install = this.force_install or this.skip_verify or brk: {
+ std.mem.copy(u8, this.destination_dir_subpath_buf[name.len..], std.fs.path.sep_str ++ "package.json");
+ this.destination_dir_subpath_buf[name.len + "/package.json".len] = 0;
+ var package_json_path = this.destination_dir_subpath_buf[0 .. name.len + "/package.json".len :0];
+ defer this.destination_dir_subpath_buf[name.len] = 0;
+ const file = this.node_modules_folder.openFileZ(package_json_path, .{ .read = true }) catch break :brk true;
+ file.close();
+
+ break :brk false;
+ };
+ this.summary.skipped += @as(u32, @boolToInt(!needs_install));
+
+ if (needs_install) {
+ if (!this.skip_delete) this.node_modules_folder.deleteFileZ(destination_dir_subpath) catch {};
+
+ std.os.symlinkatZ(folderZ, this.node_modules_folder.fd, destination_dir_subpath) catch |err| {
+ Output.prettyErrorln(
+ "<r><red>error<r>: <b><red>{s}<r> installing <b>{s}<r>",
+ .{ err, this.names[package_id].slice(buf) },
+ );
+ this.summary.fail += 1;
+ return;
+ };
+ this.summary.success += 1;
+ this.successfully_installed.set(package_id);
+ }
+ },
else => {},
}
}
@@ -6449,7 +6624,7 @@ pub const PackageManager = struct {
try lockfile.initEmpty(ctx.allocator);
var new_root: Lockfile.Package = undefined;
if (manager.options.enable.install_dev_dependencies) {
- try Lockfile.Package.parse(
+ try Lockfile.Package.parseMain(
&lockfile,
&new_root,
ctx.allocator,
@@ -6464,7 +6639,7 @@ pub const PackageManager = struct {
},
);
} else {
- try Lockfile.Package.parse(
+ try Lockfile.Package.parseMain(
&lockfile,
&new_root,
ctx.allocator,
@@ -6569,7 +6744,7 @@ pub const PackageManager = struct {
try manager.lockfile.initEmpty(ctx.allocator);
if (manager.options.enable.install_dev_dependencies) {
- try Lockfile.Package.parse(
+ try Lockfile.Package.parseMain(
manager.lockfile,
&root,
ctx.allocator,
@@ -6584,7 +6759,7 @@ pub const PackageManager = struct {
},
);
} else {
- try Lockfile.Package.parse(
+ try Lockfile.Package.parseMain(
manager.lockfile,
&root,
ctx.allocator,
diff --git a/src/install/resolution.zig b/src/install/resolution.zig
index ea75486a7..6a90048e5 100644
--- a/src/install/resolution.zig
+++ b/src/install/resolution.zig
@@ -246,6 +246,9 @@ pub const Resolution = extern struct {
gitlab: Repository,
workspace: String,
+
+ /// global link
+ /// not implemented yet
symlink: String,
single_file_module: String,