diff --git a/src/cli/update_interactive_command.zig b/src/cli/update_interactive_command.zig index a5e5640041..a06308e400 100644 --- a/src/cli/update_interactive_command.zig +++ b/src/cli/update_interactive_command.zig @@ -1981,11 +1981,40 @@ fn updateNamedCatalog( } fn preserveVersionPrefix(original_version: string, new_version: string, allocator: std.mem.Allocator) !string { - if (original_version.len > 0) { - const first_char = original_version[0]; + if (original_version.len > 1) { + var orig_version = original_version; + var alias: ?string = null; + + // Preserve npm: prefix + if (strings.withoutPrefixIfPossibleComptime(original_version, "npm:")) |after_npm| { + if (strings.lastIndexOfChar(after_npm, '@')) |i| { + alias = after_npm[0..i]; + if (i + 2 < after_npm.len) { + orig_version = after_npm[i + 1 ..]; + } + } else { + alias = after_npm; + } + } + + // Preserve other version prefixes + const first_char = orig_version[0]; if (first_char == '^' or first_char == '~' or first_char == '>' or first_char == '<' or first_char == '=') { + const second_char = orig_version[1]; + if ((first_char == '>' or first_char == '<') and second_char == '=') { + if (alias) |a| { + return try std.fmt.allocPrint(allocator, "npm:{s}@{c}={s}", .{ a, first_char, new_version }); + } + return try std.fmt.allocPrint(allocator, "{c}={s}", .{ first_char, new_version }); + } + if (alias) |a| { + return try std.fmt.allocPrint(allocator, "npm:{s}@{c}{s}", .{ a, first_char, new_version }); + } return try std.fmt.allocPrint(allocator, "{c}{s}", .{ first_char, new_version }); } + if (alias) |a| { + return try std.fmt.allocPrint(allocator, "npm:{s}@{s}", .{ a, new_version }); + } } return try allocator.dupe(u8, new_version); } diff --git a/test/cli/update_interactive_formatting.test.ts b/test/cli/update_interactive_formatting.test.ts index 8e54570781..cb97d2df09 100644 --- a/test/cli/update_interactive_formatting.test.ts +++ b/test/cli/update_interactive_formatting.test.ts @@ -1861,6 +1861,51 @@ registry = "${registryUrl}" expect(packageJson.dependencies["dep-with-tags"]).toBe("1.0.0"); }); + it("should preserve npm: alias prefix when updating packages", async () => { + const dir = tempDirWithFiles("update-interactive-npm-alias", { + "bunfig.toml": `[install] +cache = false +registry = "${registryUrl}" +`, + "package.json": JSON.stringify({ + name: "test-project", + version: "1.0.0", + dependencies: { + "my-alias": "npm:no-deps@1.0.0", + "@my/alias": "npm:@types/no-deps@^1.0.0", + }, + }), + }); + + await using install = Bun.spawn({ + cmd: [bunExe(), "install"], + cwd: dir, + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + expect(await install.exited).toBe(0); + + await using update = Bun.spawn({ + cmd: [bunExe(), "update", "-i", "--latest"], + cwd: dir, + env: bunEnv, + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + }); + + update.stdin.write("a\n"); + update.stdin.end(); + + const exitCode = await update.exited; + expect(exitCode).toBe(0); + + const packageJson = await Bun.file(join(dir, "package.json")).json(); + expect(packageJson.dependencies["my-alias"]).toBe("npm:no-deps@2.0.0"); + expect(packageJson.dependencies["@my/alias"]).toBe("npm:@types/no-deps@^2.0.0"); + }); + it("interactive update with mixed dependency types", async () => { const dir = tempDirWithFiles("update-interactive-mixed", { "bunfig.toml": `[install] @@ -1891,7 +1936,7 @@ registry = "${registryUrl}" name: "@test/workspace1", dependencies: { "a-dep": "catalog:", - "@test/workspace2": "workspace:*", // Workspace dependency + "@test/workspace2": "workspace:*", }, devDependencies: { "no-deps": "^1.0.0",