From 5108e3e0d9f7806c3884e5a678a81dedd61791bc Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 2 Sep 2024 15:12:00 -0700 Subject: [PATCH] Add snapshot tests for dependency/version parsing (#13658) --- src/bun.js/bindings/bindings.zig | 2 + src/install/dependency.zig | 143 +++++++ src/install/install.zig | 99 ++++- src/install/semver.zig | 5 + src/js/internal-for-testing.ts | 8 + .../bun-install-dep.test.ts.snap | 397 ++++++++++++++++++ test/cli/install/bun-install-dep.test.ts | 70 +++ 7 files changed, 716 insertions(+), 8 deletions(-) create mode 100644 test/cli/install/__snapshots__/bun-install-dep.test.ts.snap create mode 100644 test/cli/install/bun-install-dep.test.ts diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 752fb353dfbe82..eeb62dd6b8bc0e 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -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)); } diff --git a/src/install/dependency.zig b/src/install/dependency.zig index 7508cd9ed2aec1..d21b8be3198d9c 100644 --- a/src/install/dependency.zig +++ b/src/install/dependency.zig @@ -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, @@ -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; } @@ -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)); @@ -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 { @@ -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 = .{}; diff --git a/src/install/install.zig b/src/install/install.zig index e7f5b562f39ead..31c1af777f667b 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -9638,6 +9638,64 @@ 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, @@ -9645,6 +9703,17 @@ pub const PackageManager = struct { 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 @@ -9690,10 +9759,17 @@ pub const PackageManager = struct { &SlicedString.init(input, value), log, ) orelse { - Output.prettyErrorln("error: 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( @@ -9714,10 +9790,17 @@ pub const PackageManager = struct { .npm => version.value.npm.name.eql(placeholder, input, input), else => false, }) { - Output.prettyErrorln("error: 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{ diff --git a/src/install/semver.zig b/src/install/semver.zig index cbd76749945f51..ef93a68be5f353 100644 --- a/src/install/semver.zig +++ b/src/install/semver.zig @@ -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) { diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index e51917e33c1f3e..96f0d042704fc7 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -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); diff --git a/test/cli/install/__snapshots__/bun-install-dep.test.ts.snap b/test/cli/install/__snapshots__/bun-install-dep.test.ts.snap new file mode 100644 index 00000000000000..94cd72502d6934 --- /dev/null +++ b/test/cli/install/__snapshots__/bun-install-dep.test.ts.snap @@ -0,0 +1,397 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`npa @scoped/package 1`] = ` +{ + "name": "@scoped/package", + "version": { + "name": "@scoped/package", + "tag": "latest", + "type": "dist_tag", + }, +} +`; + +exports[`npa @scoped/package@1.0.0 1`] = ` +{ + "name": "@scoped/package", + "version": { + "alias": false, + "name": "@scoped/package", + "type": "npm", + "version": "==1.0.0", + }, +} +`; + +exports[`npa @scoped/package@1.0.0-beta.1 1`] = ` +{ + "name": "@scoped/package", + "version": { + "alias": false, + "name": "@scoped/package", + "type": "npm", + "version": "==1.0.0-beta.1", + }, +} +`; + +exports[`npa @scoped/package@1.0.0-beta.1+build.123 1`] = ` +{ + "name": "@scoped/package", + "version": { + "alias": false, + "name": "@scoped/package", + "type": "npm", + "version": "==1.0.0-beta.1+build.123", + }, +} +`; + +exports[`npa package 1`] = ` +{ + "name": "package", + "version": { + "name": "package", + "tag": "latest", + "type": "dist_tag", + }, +} +`; + +exports[`npa package@1.0.0 1`] = ` +{ + "name": "package", + "version": { + "alias": false, + "name": "package", + "type": "npm", + "version": "==1.0.0", + }, +} +`; + +exports[`npa package@1.0.0-beta.1 1`] = ` +{ + "name": "package", + "version": { + "alias": false, + "name": "package", + "type": "npm", + "version": "==1.0.0-beta.1", + }, +} +`; + +exports[`npa package@1.0.0-beta.1+build.123 1`] = ` +{ + "name": "package", + "version": { + "alias": false, + "name": "package", + "type": "npm", + "version": "==1.0.0-beta.1+build.123", + }, +} +`; + +exports[`npa bitbucket:dylan-conway/public-install-test 1`] = ` +{ + "name": "", + "version": { + "owner": "", + "ref": "", + "repo": "bitbucket:dylan-conway/public-install-test", + "type": "git", + }, +} +`; + +exports[`npa bitbucket.org:dylan-conway/public-install-test 1`] = ` +{ + "name": "", + "version": { + "owner": "", + "ref": "", + "repo": "bitbucket.org:dylan-conway/public-install-test", + "type": "git", + }, +} +`; + +exports[`npa bitbucket.com:dylan-conway/public-install-test 1`] = ` +{ + "name": "", + "version": { + "owner": "", + "ref": "", + "repo": "bitbucket.com:dylan-conway/public-install-test", + "type": "git", + }, +} +`; + +exports[`npa git@bitbucket.org:dylan-conway/public-install-test 1`] = ` +{ + "name": "", + "version": { + "owner": "", + "ref": "", + "repo": "git@bitbucket.org:dylan-conway/public-install-test", + "type": "git", + }, +} +`; + +exports[`npa foo/bar 1`] = ` +{ + "name": "", + "version": { + "owner": "foo", + "ref": "", + "repo": "bar", + "type": "github", + }, +} +`; + +exports[`npa gitlab:dylan-conway/public-install-test 1`] = ` +{ + "name": "", + "version": { + "owner": "", + "ref": "", + "repo": "gitlab:dylan-conway/public-install-test", + "type": "git", + }, +} +`; + +exports[`npa gitlab.com:dylan-conway/public-install-test 1`] = ` +{ + "name": "", + "version": { + "owner": "", + "ref": "", + "repo": "gitlab.com:dylan-conway/public-install-test", + "type": "git", + }, +} +`; + +exports[`npa http://localhost:5000/no-deps/-/no-deps-2.0.0.tgz 1`] = ` +{ + "name": "", + "version": { + "name": "", + "type": "tarball", + "url": "http://localhost:5000/no-deps/-/no-deps-2.0.0.tgz", + }, +} +`; + +exports[`npa https://registry.npmjs.org/no-deps/-/no-deps-2.0.0.tgz 1`] = ` +{ + "name": "", + "version": { + "name": "", + "type": "tarball", + "url": "https://registry.npmjs.org/no-deps/-/no-deps-2.0.0.tgz", + }, +} +`; + +exports[`npa file:./path/to/tarball.tgz 1`] = ` +{ + "name": "", + "version": { + "name": "", + "path": "./path/to/tarball.tgz", + "type": "tarball", + }, +} +`; + +exports[`npa ./path/to/tarball.tgz 1`] = ` +{ + "name": "", + "version": { + "name": "", + "path": "./path/to/tarball.tgz", + "type": "tarball", + }, +} +`; + +exports[`npa foo/bar 2`] = ` +{ + "name": "", + "version": { + "owner": "foo", + "ref": "", + "repo": "bar", + "type": "github", + }, +} +`; + +exports[`npa github:dylan-conway/public-install-test 1`] = ` +{ + "name": "", + "version": { + "owner": "dylan-conway", + "ref": "", + "repo": "public-install-test", + "type": "github", + }, +} +`; + +exports[`npa git@github.com:dylan-conway/public-install-test 1`] = ` +{ + "name": "", + "version": { + "owner": "", + "ref": "", + "repo": "git@github.com:dylan-conway/public-install-test", + "type": "git", + }, +} +`; + +exports[`npa https://github.com/dylan-conway/public-install-test 1`] = ` +{ + "name": "", + "version": { + "owner": "dylan-conway", + "ref": "", + "repo": "public-install-test", + "type": "github", + }, +} +`; + +exports[`npa https://github.com/dylan-conway/public-install-test.git 1`] = ` +{ + "name": "", + "version": { + "owner": "dylan-conway", + "ref": "", + "repo": "public-install-test", + "type": "github", + }, +} +`; + +exports[`npa https://github.com/dylan-conway/public-install-test.git#semver:^1.0.0 1`] = ` +{ + "name": "", + "version": { + "owner": "", + "ref": "semver:^1.0.0", + "repo": "https://github.com/dylan-conway/public-install-test.git", + "type": "git", + }, +} +`; + +exports[`dependencies: {"foo": "1.2.3"} 1`] = ` +{ + "alias": false, + "name": "foo", + "type": "npm", + "version": "==1.2.3-foo", +} +`; + +exports[`dependencies: {"foo": "latest"} 1`] = ` +{ + "name": "foo", + "tag": "latest", + "type": "dist_tag", +} +`; + +exports[`dependencies: {"foo": "workspace:*"} 1`] = ` +{ + "name": "*foo", + "type": "workspace", +} +`; + +exports[`dependencies: {"foo": "workspace:^1.0.0"} 1`] = ` +{ + "name": "^1.0.0foo", + "type": "workspace", +} +`; + +exports[`dependencies: {"foo": "workspace:1.0.0"} 1`] = ` +{ + "name": "1.0.0foo", + "type": "workspace", +} +`; + +exports[`dependencies: {"foo": "workspace:1.0.0-beta.1"} 1`] = ` +{ + "name": "1.0.0-beta.1foo", + "type": "workspace", +} +`; + +exports[`dependencies: {"foo": "workspace:1.0.0-beta.1+build.123"} 1`] = ` +{ + "name": "1.0.0-beta.1+build.123foo", + "type": "workspace", +} +`; + +exports[`dependencies: {"foo": "workspace:1.0.0-beta.1+build.123"} 2`] = ` +{ + "name": "1.0.0-beta.1+build.123foo", + "type": "workspace", +} +`; + +exports[`dependencies: {"foo": "workspace:1.0.0-beta.1+build.123"} 3`] = ` +{ + "name": "1.0.0-beta.1+build.123foo", + "type": "workspace", +} +`; + +exports[`dependencies: {"bar": "^1.0.0"} 1`] = ` +{ + "alias": false, + "name": "bar", + "type": "npm", + "version": ">=1.0.0-bar <2.0.0", +} +`; + +exports[`dependencies: {"bar": "~1.0.0"} 1`] = ` +{ + "alias": false, + "name": "bar", + "type": "npm", + "version": ">=1.0.0-bar <1.1.0", +} +`; + +exports[`dependencies: {"bar": "> 1.0.0 < 2.0.0"} 1`] = ` +{ + "alias": false, + "name": "bar", + "type": "npm", + "version": ">1.0.0 && <2.0.0-bar", +} +`; + +exports[`dependencies: {"bar": "1.0.0 - 2.0.0"} 1`] = ` +{ + "alias": false, + "name": "bar", + "type": "npm", + "version": ">=1.0.0 <=2.0.0-bar", +} +`; diff --git a/test/cli/install/bun-install-dep.test.ts b/test/cli/install/bun-install-dep.test.ts new file mode 100644 index 00000000000000..6e0ce18b650c78 --- /dev/null +++ b/test/cli/install/bun-install-dep.test.ts @@ -0,0 +1,70 @@ +import { test, expect, describe } from "bun:test"; +import { npa } from "bun:internal-for-testing"; + +const bitbucket = [ + "bitbucket:dylan-conway/public-install-test", + "bitbucket.org:dylan-conway/public-install-test", + "bitbucket.com:dylan-conway/public-install-test", + "git@bitbucket.org:dylan-conway/public-install-test", +]; + +const tarball_remote = [ + "http://localhost:5000/no-deps/-/no-deps-2.0.0.tgz", + "https://registry.npmjs.org/no-deps/-/no-deps-2.0.0.tgz", +]; + +const local_tarball = ["file:./path/to/tarball.tgz", "./path/to/tarball.tgz"]; +const github = ["foo/bar"]; +const folder = ["file:./path/to/folder"]; + +const gitlab = ["gitlab:dylan-conway/public-install-test", "gitlab.com:dylan-conway/public-install-test"]; + +const all = [ + "@scoped/package", + "@scoped/package@1.0.0", + "@scoped/package@1.0.0-beta.1", + "@scoped/package@1.0.0-beta.1+build.123", + "package", + "package@1.0.0", + "package@1.0.0-beta.1", + "package@1.0.0-beta.1+build.123", + ...bitbucket, + ...github, + ...gitlab, + ...tarball_remote, + ...local_tarball, + ...github, + "github:dylan-conway/public-install-test", + "git@github.com:dylan-conway/public-install-test", + "https://github.com/dylan-conway/public-install-test", + "https://github.com/dylan-conway/public-install-test.git", + "https://github.com/dylan-conway/public-install-test.git#semver:^1.0.0", +]; + +test.each(all)("npa %s", dep => { + expect(npa(dep)).toMatchSnapshot(); +}); + +const pkgJsonLike = [ + ["foo", "1.2.3"], + ["foo", "latest"], + ["foo", "workspace:*"], + ["foo", "workspace:^1.0.0"], + ["foo", "workspace:1.0.0"], + ["foo", "workspace:1.0.0-beta.1"], + ["foo", "workspace:1.0.0-beta.1+build.123"], + ["foo", "workspace:1.0.0-beta.1+build.123"], + ["foo", "workspace:1.0.0-beta.1+build.123"], + ["bar", "^1.0.0"], + ["bar", "~1.0.0"], + ["bar", "> 1.0.0 < 2.0.0"], + ["bar", "1.0.0 - 2.0.0"], +]; + +test.each(pkgJsonLike)('dependencies: {"%s": "%s"}', (name, version) => { + expect(npa(name, version)).toMatchSnapshot(); +}); + +test("bad", () => { + expect(() => npa("-123!}{P}{!P#$s")).toThrow(); +});