aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/api/schema.d.ts1
-rw-r--r--src/api/schema.js10
-rw-r--r--src/api/schema.peechy1
-rw-r--r--src/api/schema.zig10
-rw-r--r--src/bunfig.zig8
-rw-r--r--src/install/install.zig53
-rw-r--r--src/install/lockfile.zig32
-rw-r--r--test/cli/install/bun-add.test.ts55
8 files changed, 155 insertions, 15 deletions
diff --git a/src/api/schema.d.ts b/src/api/schema.d.ts
index ac6183878..2a86340ad 100644
--- a/src/api/schema.d.ts
+++ b/src/api/schema.d.ts
@@ -710,6 +710,7 @@ export interface BunInstall {
global_dir?: string;
global_bin_dir?: string;
frozen_lockfile?: boolean;
+ exact?: boolean;
}
export interface ClientServerModule {
diff --git a/src/api/schema.js b/src/api/schema.js
index 270eb9a62..f1e68031e 100644
--- a/src/api/schema.js
+++ b/src/api/schema.js
@@ -3048,6 +3048,10 @@ function decodeBunInstall(bb) {
result["frozen_lockfile"] = !!bb.readByte();
break;
+ case 20:
+ result["exact"] = !!bb.readByte();
+ break;
+
default:
throw new Error("Attempted to parse invalid message");
}
@@ -3174,6 +3178,12 @@ function encodeBunInstall(message, bb) {
bb.writeByte(19);
bb.writeByte(value);
}
+
+ var value = message["exact"];
+ if (value != null) {
+ bb.writeByte(20);
+ bb.writeByte(value);
+ }
bb.writeByte(0);
}
diff --git a/src/api/schema.peechy b/src/api/schema.peechy
index 6d28381c4..a172606f7 100644
--- a/src/api/schema.peechy
+++ b/src/api/schema.peechy
@@ -591,6 +591,7 @@ message BunInstall {
string global_dir = 17;
string global_bin_dir = 18;
bool frozen_lockfile = 19;
+ bool exact = 20;
}
struct ClientServerModule {
diff --git a/src/api/schema.zig b/src/api/schema.zig
index 2de80d42c..ec8efa9f6 100644
--- a/src/api/schema.zig
+++ b/src/api/schema.zig
@@ -2904,6 +2904,9 @@ pub const Api = struct {
/// frozen_lockfile
frozen_lockfile: ?bool = null,
+ /// exact
+ exact: ?bool = null,
+
pub fn decode(reader: anytype) anyerror!BunInstall {
var this = std.mem.zeroes(BunInstall);
@@ -2970,6 +2973,9 @@ pub const Api = struct {
19 => {
this.frozen_lockfile = try reader.readValue(bool);
},
+ 20 => {
+ this.exact = try reader.readValue(bool);
+ },
else => {
return error.InvalidMessage;
},
@@ -3055,6 +3061,10 @@ pub const Api = struct {
try writer.writeFieldID(19);
try writer.writeInt(@as(u8, @intFromBool(frozen_lockfile)));
}
+ if (this.exact) |exact| {
+ try writer.writeFieldID(20);
+ try writer.writeInt(@as(u8, @intFromBool(exact)));
+ }
try writer.endMessage();
}
};
diff --git a/src/bunfig.zig b/src/bunfig.zig
index 597fb0985..1244f52b8 100644
--- a/src/bunfig.zig
+++ b/src/bunfig.zig
@@ -259,6 +259,14 @@ pub const Bunfig = struct {
}
}
+ if (json.get("exact")) |exact_install_expr| {
+ try this.expect(exact_install_expr, .e_boolean);
+
+ if (exact_install_expr.asBool().?) {
+ install.exact = true;
+ }
+ }
+
if (json.get("prefer")) |prefer_expr| {
try this.expect(prefer_expr, .e_string);
diff --git a/src/install/install.zig b/src/install/install.zig
index 22068bbf3..9465c4897 100644
--- a/src/install/install.zig
+++ b/src/install/install.zig
@@ -4461,6 +4461,10 @@ pub const PackageManager = struct {
this.remote_package_features.peer_dependencies = save;
}
+ if (bun_install.exact) |exact| {
+ this.enable.exact_versions = exact;
+ }
+
if (bun_install.production) |production| {
if (production) {
this.local_package_features.dev_dependencies = false;
@@ -4569,6 +4573,10 @@ pub const PackageManager = struct {
this.scope.url = URL.parse(cli.registry);
}
+ if (cli.exact) {
+ this.enable.exact_versions = true;
+ }
+
if (cli.token.len > 0) {
this.scope.token = cli.token;
}
@@ -4755,6 +4763,8 @@ pub const PackageManager = struct {
force_save_lockfile: bool = false,
force_install: bool = false,
+
+ exact_versions: bool = false,
};
};
@@ -4802,6 +4812,7 @@ pub const PackageManager = struct {
updates: []UpdateRequest,
current_package_json: *JSAst.Expr,
dependency_list: string,
+ exact_versions: bool,
) !void {
const G = JSAst.G;
@@ -4989,9 +5000,14 @@ pub const PackageManager = struct {
if (update.e_string) |e_string| {
e_string.data = switch (update.resolution.tag) {
.npm => if (update.version.tag == .dist_tag and update.version.literal.isEmpty())
- std.fmt.allocPrint(allocator, "^{}", .{
- update.resolution.value.npm.version.fmt(update.version_buf),
- }) catch unreachable
+ switch (exact_versions) {
+ false => std.fmt.allocPrint(allocator, "^{}", .{
+ update.resolution.value.npm.version.fmt(update.version_buf),
+ }) catch unreachable,
+ true => std.fmt.allocPrint(allocator, "{}", .{
+ update.resolution.value.npm.version.fmt(update.version_buf),
+ }) catch unreachable,
+ }
else
null,
.uninitialized => switch (update.version.tag) {
@@ -5709,6 +5725,7 @@ pub const PackageManager = struct {
const add_params = install_params_ ++ [_]ParamType{
clap.parseParam("-d, --development Add dependency to \"devDependencies\"") catch unreachable,
clap.parseParam("--optional Add dependency to \"optionalDependencies\"") catch unreachable,
+ clap.parseParam("--exact Add the exact version instead of the ^range") catch unreachable,
clap.parseParam("<POS> ... \"name\" or \"name@version\" of packages to install") catch unreachable,
};
@@ -5759,6 +5776,8 @@ pub const PackageManager = struct {
no_optional: bool = false,
omit: Omit = Omit{},
+ exact: bool = false,
+
const Omit = struct {
dev: bool = false,
optional: bool = true,
@@ -5837,6 +5856,7 @@ pub const PackageManager = struct {
if (comptime subcommand == .add) {
cli.development = args.flag("--development");
cli.optional = args.flag("--optional");
+ cli.exact = args.flag("--exact");
}
// for (args.options("--omit")) |omit| {
@@ -6293,7 +6313,13 @@ pub const PackageManager = struct {
manager.to_remove = updates;
},
.link, .add, .update => {
- try PackageJSONEditor.edit(ctx.allocator, updates, &current_package_json, dependency_list);
+ try PackageJSONEditor.edit(
+ ctx.allocator,
+ updates,
+ &current_package_json,
+ dependency_list,
+ manager.options.enable.exact_versions,
+ );
manager.package_json_updates = updates;
},
else => {},
@@ -6346,7 +6372,13 @@ pub const PackageManager = struct {
return;
};
- try PackageJSONEditor.edit(ctx.allocator, updates, &current_package_json, dependency_list);
+ try PackageJSONEditor.edit(
+ ctx.allocator,
+ updates,
+ &current_package_json,
+ dependency_list,
+ manager.options.enable.exact_versions,
+ );
var buffer_writer_two = try JSPrinter.BufferWriter.init(ctx.allocator);
try buffer_writer_two.buffer.list.ensureTotalCapacity(ctx.allocator, new_package_json_source.len + 1);
buffer_writer_two.append_newline =
@@ -7031,7 +7063,10 @@ pub const PackageManager = struct {
) !PackageInstall.Summary {
var lockfile = lockfile_;
if (!this.options.local_package_features.dev_dependencies) {
- lockfile = try lockfile.maybeCloneFilteringRootPackages(this.options.local_package_features);
+ lockfile = try lockfile.maybeCloneFilteringRootPackages(
+ this.options.local_package_features,
+ this.options.enable.exact_versions,
+ );
}
var root_node: *Progress.Node = undefined;
@@ -7582,7 +7617,11 @@ pub const PackageManager = struct {
const needs_clean_lockfile = had_any_diffs or needs_new_lockfile or manager.package_json_updates.len > 0;
var did_meta_hash_change = needs_clean_lockfile;
if (needs_clean_lockfile) {
- manager.lockfile = try manager.lockfile.cleanWithLogger(manager.package_json_updates, manager.log);
+ manager.lockfile = try manager.lockfile.cleanWithLogger(
+ manager.package_json_updates,
+ manager.log,
+ manager.options.enable.exact_versions,
+ );
}
if (manager.lockfile.packages.len > 0) {
diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig
index a51d2b2ee..7d21860ef 100644
--- a/src/install/lockfile.zig
+++ b/src/install/lockfile.zig
@@ -538,6 +538,7 @@ pub const Tree = struct {
pub fn maybeCloneFilteringRootPackages(
old: *Lockfile,
features: Features,
+ exact_versions: bool,
) !*Lockfile {
const old_root_dependenices_list = old.packages.items(.dependencies)[0];
var old_root_resolutions = old.packages.items(.resolutions)[0];
@@ -555,10 +556,10 @@ pub fn maybeCloneFilteringRootPackages(
if (!any_changes) return old;
- return try old.clean(&.{});
+ return try old.clean(&.{}, exact_versions);
}
-fn preprocessUpdateRequests(old: *Lockfile, updates: []PackageManager.UpdateRequest) !void {
+fn preprocessUpdateRequests(old: *Lockfile, updates: []PackageManager.UpdateRequest, exact_versions: bool) !void {
const root_deps_list: Lockfile.DependencySlice = old.packages.items(.dependencies)[0];
if (@as(usize, root_deps_list.off) < old.buffers.dependencies.items.len) {
var string_builder = old.stringBuilder();
@@ -575,7 +576,10 @@ fn preprocessUpdateRequests(old: *Lockfile, updates: []PackageManager.UpdateRequ
if (dep.name_hash == String.Builder.stringHash(update.name)) {
if (old_resolution > old.packages.len) continue;
const res = resolutions_of_yore[old_resolution];
- const len = std.fmt.count("^{}", .{res.value.npm.fmt(old.buffers.string_bytes.items)});
+ const len = switch (exact_versions) {
+ false => std.fmt.count("^{}", .{res.value.npm.fmt(old.buffers.string_bytes.items)}),
+ true => std.fmt.count("{}", .{res.value.npm.fmt(old.buffers.string_bytes.items)}),
+ };
if (len >= String.max_inline_len) {
string_builder.cap += len;
}
@@ -603,7 +607,10 @@ fn preprocessUpdateRequests(old: *Lockfile, updates: []PackageManager.UpdateRequ
if (dep.name_hash == String.Builder.stringHash(update.name)) {
if (old_resolution > old.packages.len) continue;
const res = resolutions_of_yore[old_resolution];
- var buf = std.fmt.bufPrint(&temp_buf, "^{}", .{res.value.npm.fmt(old.buffers.string_bytes.items)}) catch break;
+ var buf = switch (exact_versions) {
+ false => std.fmt.bufPrint(&temp_buf, "^{}", .{res.value.npm.fmt(old.buffers.string_bytes.items)}) catch break,
+ true => std.fmt.bufPrint(&temp_buf, "{}", .{res.value.npm.fmt(old.buffers.string_bytes.items)}) catch break,
+ };
const external_version = string_builder.append(ExternalString, buf);
const sliced = external_version.value.sliced(old.buffers.string_bytes.items);
dep.version = Dependency.parse(
@@ -622,7 +629,11 @@ fn preprocessUpdateRequests(old: *Lockfile, updates: []PackageManager.UpdateRequ
}
}
}
-pub fn clean(old: *Lockfile, updates: []PackageManager.UpdateRequest) !*Lockfile {
+pub fn clean(
+ old: *Lockfile,
+ updates: []PackageManager.UpdateRequest,
+ exact_versions: bool,
+) !*Lockfile {
// This is wasteful, but we rarely log anything so it's fine.
var log = logger.Log.init(bun.default_allocator);
defer {
@@ -632,17 +643,22 @@ pub fn clean(old: *Lockfile, updates: []PackageManager.UpdateRequest) !*Lockfile
log.deinit();
}
- return old.cleanWithLogger(updates, &log);
+ return old.cleanWithLogger(updates, &log, exact_versions);
}
-pub fn cleanWithLogger(old: *Lockfile, updates: []PackageManager.UpdateRequest, log: *logger.Log) !*Lockfile {
+pub fn cleanWithLogger(
+ old: *Lockfile,
+ updates: []PackageManager.UpdateRequest,
+ log: *logger.Log,
+ exact_versions: bool,
+) !*Lockfile {
const old_trusted_dependencies = old.trusted_dependencies;
const old_scripts = old.scripts;
// We will only shrink the number of packages here.
// never grow
if (updates.len > 0) {
- try old.preprocessUpdateRequests(updates);
+ try old.preprocessUpdateRequests(updates, exact_versions);
}
// Deduplication works like this
diff --git a/test/cli/install/bun-add.test.ts b/test/cli/install/bun-add.test.ts
index 79804f0e0..9dd38c8cd 100644
--- a/test/cli/install/bun-add.test.ts
+++ b/test/cli/install/bun-add.test.ts
@@ -303,6 +303,61 @@ it("should add dependency with capital letters", async () => {
await access(join(package_dir, "bun.lockb"));
});
+it("should add exact version", async () => {
+ const urls: string[] = [];
+ setHandler(dummyRegistry(urls));
+ await writeFile(
+ join(package_dir, "package.json"),
+ JSON.stringify({
+ name: "foo",
+ version: "0.0.1",
+ }),
+ );
+ const { stdout, stderr, exited } = spawn({
+ cmd: [bunExe(), "add", "--exact", "BaR"],
+ cwd: package_dir,
+ stdout: null,
+ stdin: "pipe",
+ stderr: "pipe",
+ env,
+ });
+ expect(stderr).toBeDefined();
+ const err = await new Response(stderr).text();
+ expect(err).toContain("Saved lockfile");
+ expect(stdout).toBeDefined();
+ const out = await new Response(stdout).text();
+ expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
+ "",
+ " installed BaR@0.0.2",
+ "",
+ "",
+ " 1 packages installed",
+ ]);
+ expect(await exited).toBe(0);
+ expect(urls.sort()).toEqual([`${root_url}/BaR`, `${root_url}/BaR-0.0.2.tgz`]);
+ expect(requested).toBe(2);
+ expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "BaR"]);
+ expect(await readdirSorted(join(package_dir, "node_modules", "BaR"))).toEqual(["package.json"]);
+ expect(await file(join(package_dir, "node_modules", "BaR", "package.json")).json()).toEqual({
+ name: "bar",
+ version: "0.0.2",
+ });
+ expect(await file(join(package_dir, "package.json")).text()).toEqual(
+ JSON.stringify(
+ {
+ name: "foo",
+ version: "0.0.1",
+ dependencies: {
+ BaR: "0.0.2",
+ },
+ },
+ null,
+ 2,
+ ),
+ );
+ await access(join(package_dir, "bun.lockb"));
+});
+
it("should add dependency with specified semver", async () => {
const urls: string[] = [];
setHandler(