Skip to content

Commit

Permalink
Add snapshot tests for dependency/version parsing (#13658)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jarred-Sumner authored Sep 2, 2024
1 parent bd3e62d commit 5108e3e
Show file tree
Hide file tree
Showing 7 changed files with 716 additions and 8 deletions.
2 changes: 2 additions & 0 deletions src/bun.js/bindings/bindings.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3975,6 +3975,8 @@ pub const JSValue = enum(JSValueReprInt) {
putZigString(value, global, key, result);
} else if (Elem == bun.String) {
putBunString(value, global, key, result);
} else if (std.meta.Elem(Key) == u8) {
putZigString(value, global, &ZigString.init(key), result);
} else {
@compileError("Unsupported key type in put(). Expected ZigString or bun.String, got " ++ @typeName(Elem));
}
Expand Down
143 changes: 143 additions & 0 deletions src/install/dependency.zig
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const std = @import("std");
const string = @import("../string_types.zig").string;
const strings = @import("../string_immutable.zig");
const Dependency = @This();
const JSC = bun.JSC;

const URI = union(Tag) {
local: String,
Expand Down Expand Up @@ -288,6 +289,66 @@ pub const Version = struct {
literal: String = .{},
value: Value = .{ .uninitialized = {} },

pub fn toJS(dep: *const Version, buf: []const u8, globalThis: *JSC.JSGlobalObject) JSC.JSValue {
const object = JSC.JSValue.createEmptyObject(globalThis, 2);
object.put(globalThis, "type", bun.String.static(@tagName(dep.tag)).toJS(globalThis));

switch (dep.tag) {
.dist_tag => {
object.put(globalThis, "name", dep.value.dist_tag.name.toJS(buf, globalThis));
object.put(globalThis, "tag", dep.value.dist_tag.tag.toJS(buf, globalThis));
},
.folder => {
object.put(globalThis, "folder", dep.value.folder.toJS(buf, globalThis));
},
.git => {
object.put(globalThis, "owner", dep.value.git.owner.toJS(buf, globalThis));
object.put(globalThis, "repo", dep.value.git.repo.toJS(buf, globalThis));
object.put(globalThis, "ref", dep.value.git.committish.toJS(buf, globalThis));
},
.github => {
object.put(globalThis, "owner", dep.value.github.owner.toJS(buf, globalThis));
object.put(globalThis, "repo", dep.value.github.repo.toJS(buf, globalThis));
object.put(globalThis, "ref", dep.value.github.committish.toJS(buf, globalThis));
},
.npm => {
object.put(globalThis, "name", dep.value.npm.name.toJS(buf, globalThis));
var version_str = bun.String.createFormat("{}", .{dep.value.npm.version.fmt(buf)}) catch {
globalThis.throwOutOfMemory();
return .zero;
};
object.put(
globalThis,
"version",
version_str.transferToJS(globalThis),
);
object.put(globalThis, "alias", JSC.JSValue.jsBoolean(dep.value.npm.is_alias));
},
.symlink => {
object.put(globalThis, "path", dep.value.symlink.toJS(buf, globalThis));
},
.workspace => {
object.put(globalThis, "name", dep.value.workspace.toJS(buf, globalThis));
},
.tarball => {
object.put(globalThis, "name", dep.value.tarball.package_name.toJS(buf, globalThis));
switch (dep.value.tarball.uri) {
.local => |*local| {
object.put(globalThis, "path", local.toJS(buf, globalThis));
},
.remote => |*remote| {
object.put(globalThis, "url", remote.toJS(buf, globalThis));
},
}
},
else => {
globalThis.throwTODO("Unsupported dependency type");
return .zero;
},
}

return object;
}
pub inline fn npm(this: *const Version) ?NpmInfo {
return if (this.tag == .npm) this.value.npm else null;
}
Expand Down Expand Up @@ -412,6 +473,18 @@ pub const Version = struct {
/// GitHub Repository (via REST API)
github = 8,

pub const map = bun.ComptimeStringMap(Tag, .{
.{ "npm", .npm },
.{ "dist_tag", .dist_tag },
.{ "tarball", .tarball },
.{ "folder", .folder },
.{ "symlink", .symlink },
.{ "workspace", .workspace },
.{ "git", .git },
.{ "github", .github },
});
pub const fromJS = map.fromJS;

pub fn cmp(this: Tag, other: Tag) std.math.Order {
// TODO: align with yarn
return std.math.order(@intFromEnum(this), @intFromEnum(other));
Expand Down Expand Up @@ -670,6 +743,17 @@ pub const Version = struct {

return .npm;
}

pub fn inferFromJS(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue {
const arguments = callframe.arguments(1).slice();
if (arguments.len == 0 or !arguments[0].isString()) {
return .undefined;
}

const tag = Tag.fromJS(globalObject, arguments[0]) orelse return .undefined;
var str = bun.String.init(@tagName(tag));
return str.transferToJS(globalObject);
}
};

pub const NpmInfo = struct {
Expand Down Expand Up @@ -1131,6 +1215,65 @@ pub fn parseWithTag(
}
}

pub fn fromJS(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue {
const arguments = callframe.arguments(2).slice();
if (arguments.len == 1) {
return bun.install.PackageManager.UpdateRequest.fromJS(globalThis, arguments[0]);
}
var arena = std.heap.ArenaAllocator.init(bun.default_allocator);
defer arena.deinit();
var stack = std.heap.stackFallback(1024, arena.allocator());
const allocator = stack.get();

const alias_value = if (arguments.len > 0) arguments[0] else .undefined;

if (!alias_value.isString()) {
return .undefined;
}
const alias_slice = alias_value.toSlice(globalThis, allocator);
defer alias_slice.deinit();

if (alias_slice.len == 0) {
return .undefined;
}

const name_value = if (arguments.len > 1) arguments[1] else .undefined;
const name_slice = name_value.toSlice(globalThis, allocator);
defer name_slice.deinit();

var name = alias_slice.slice();
var alias = alias_slice.slice();

var buf = alias;

if (name_value.isString()) {
var builder = bun.StringBuilder.initCapacity(allocator, name_slice.len + alias_slice.len) catch bun.outOfMemory();
name = builder.append(name_slice.slice());
alias = builder.append(alias_slice.slice());
buf = builder.allocatedSlice();
}

var log = logger.Log.init(allocator);
const sliced = SlicedString.init(buf, name);

const dep: Version = Dependency.parse(allocator, SlicedString.init(buf, alias).value(), null, buf, &sliced, &log) orelse {
if (log.msgs.items.len > 0) {
globalThis.throwValue(log.toJS(globalThis, bun.default_allocator, "Failed to parse dependency"));
return .zero;
}

return .undefined;
};

if (log.msgs.items.len > 0) {
globalThis.throwValue(log.toJS(globalThis, bun.default_allocator, "Failed to parse dependency"));
return .zero;
}
log.deinit();

return dep.toJS(buf, globalThis);
}

pub const Behavior = packed struct(u8) {
pub const uninitialized: Behavior = .{};

Expand Down
99 changes: 91 additions & 8 deletions src/install/install.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9638,13 +9638,82 @@ pub const PackageManager = struct {
this.resolved_name.slice(this.version_buf);
}

pub fn fromJS(globalThis: *JSC.JSGlobalObject, input: JSC.JSValue) JSC.JSValue {
var arena = std.heap.ArenaAllocator.init(bun.default_allocator);
defer arena.deinit();
var stack = std.heap.stackFallback(1024, arena.allocator());
const allocator = stack.get();
var all_positionals = std.ArrayList([]const u8).init(allocator);

var log = logger.Log.init(allocator);

if (input.isString()) {
var input_str = input.toSliceCloneWithAllocator(
globalThis,
allocator,
) orelse return .zero;
if (input_str.len > 0)
all_positionals.append(input_str.slice()) catch bun.outOfMemory();
} else if (input.isArray()) {
var iter = input.arrayIterator(globalThis);
while (iter.next()) |item| {
const slice = item.toSliceCloneWithAllocator(globalThis, allocator) orelse return .zero;
if (globalThis.hasException()) return .zero;
if (slice.len == 0) continue;
all_positionals.append(slice.slice()) catch bun.outOfMemory();
}
if (globalThis.hasException()) return .zero;
} else {
return .undefined;
}

if (all_positionals.items.len == 0) {
return .undefined;
}

var array = Array{};

const update_requests = parseWithError(allocator, &log, all_positionals.items, &array, .add, false) catch {
globalThis.throwValue(log.toJS(globalThis, bun.default_allocator, "Failed to parse dependencies"));
return .zero;
};
if (update_requests.len == 0) return .undefined;

if (log.msgs.items.len > 0) {
globalThis.throwValue(log.toJS(globalThis, bun.default_allocator, "Failed to parse dependencies"));
return .zero;
}

if (update_requests[0].failed) {
globalThis.throw("Failed to parse dependencies", .{});
return .zero;
}

var object = JSC.JSValue.createEmptyObject(globalThis, 2);
var name_str = bun.String.init(update_requests[0].name);
object.put(globalThis, "name", name_str.transferToJS(globalThis));
object.put(globalThis, "version", update_requests[0].version.toJS(update_requests[0].version_buf, globalThis));
return object;
}

pub fn parse(
allocator: std.mem.Allocator,
log: *logger.Log,
positionals: []const string,
update_requests: *Array,
subcommand: Subcommand,
) []UpdateRequest {
return parseWithError(allocator, log, positionals, update_requests, subcommand, true) catch Global.crash();
}

fn parseWithError(
allocator: std.mem.Allocator,
log: *logger.Log,
positionals: []const string,
update_requests: *Array,
subcommand: Subcommand,
fatal: bool,
) ![]UpdateRequest {
// first one is always either:
// add
// remove
Expand Down Expand Up @@ -9690,10 +9759,17 @@ pub const PackageManager = struct {
&SlicedString.init(input, value),
log,
) orelse {
Output.prettyErrorln("<r><red>error<r><d>:<r> unrecognised dependency format: {s}", .{
positional,
});
Global.crash();
if (fatal) {
Output.errGeneric("unrecognised dependency format: {s}", .{
positional,
});
} else {
log.addErrorFmt(null, logger.Loc.Empty, allocator, "unrecognised dependency format: {s}", .{
positional,
}) catch bun.outOfMemory();
}

return error.UnrecognizedDependencyFormat;
};
if (alias != null and version.tag == .git) {
if (Dependency.parseWithOptionalTag(
Expand All @@ -9714,10 +9790,17 @@ pub const PackageManager = struct {
.npm => version.value.npm.name.eql(placeholder, input, input),
else => false,
}) {
Output.prettyErrorln("<r><red>error<r><d>:<r> unrecognised dependency format: {s}", .{
positional,
});
Global.crash();
if (fatal) {
Output.errGeneric("unrecognised dependency format: {s}", .{
positional,
});
} else {
log.addErrorFmt(null, logger.Loc.Empty, allocator, "unrecognised dependency format: {s}", .{
positional,
}) catch bun.outOfMemory();
}

return error.UnrecognizedDependencyFormat;
}

var request = UpdateRequest{
Expand Down
5 changes: 5 additions & 0 deletions src/install/semver.zig
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,11 @@ pub const String = extern struct {
return @as(Pointer, @bitCast(@as(u64, @as(u63, @truncate(@as(u64, @bitCast(this)))))));
}

pub fn toJS(this: *const String, buffer: []const u8, globalThis: *JSC.JSGlobalObject) JSC.JSValue {
var str = bun.String.init(this.slice(buffer));
return str.transferToJS(globalThis);
}

// String must be a pointer because we reference it as a slice. It will become a dead pointer if it is copied.
pub fn slice(this: *const String, buf: string) string {
switch (this.bytes[max_inline_len - 1] & 128) {
Expand Down
8 changes: 8 additions & 0 deletions src/js/internal-for-testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,11 @@ export const npm_manifest_test_helpers = $zig("npm.zig", "PackageManifest.bindin
*/
parseManifest: (manifestFileName: string, registryUrl: string) => any;
};

// Like npm-package-arg, sort of https://www.npmjs.com/package/npm-package-arg
export const npa: (name: string) => Dependency = $newZigFunction("dependency.zig", "fromJS", 1);

export const npmTag: (
name: string,
) => undefined | "npm" | "dist_tag" | "tarball" | "folder" | "symlink" | "workspace" | "git" | "github" =
$newZigFunction("dependency.zig", "Version.Tag.inferFromJS", 1);
Loading

0 comments on commit 5108e3e

Please sign in to comment.