From acf36d958a5af070ffbec7a398205eaffe554ab9 Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Tue, 6 May 2025 22:16:56 -0700 Subject: [PATCH] fix(npmrc): handle BOM conversion (#18878) --- src/cli/filter_arg.zig | 2 +- src/cli/pack_command.zig | 2 +- src/ini.zig | 2 +- src/install/install.zig | 10 +- src/install/lockfile.zig | 6 +- src/install/repository.zig | 8 +- src/string_immutable.zig | 8 +- src/sys.zig | 34 +- src/transpiler.zig | 2 +- test/cli/install/bun-install-registry.test.ts | 399 +--------------- test/cli/install/npmrc.test.ts | 447 ++++++++++++++++++ 11 files changed, 496 insertions(+), 424 deletions(-) create mode 100644 test/cli/install/npmrc.test.ts diff --git a/src/cli/filter_arg.zig b/src/cli/filter_arg.zig index 49e8702ce0..87b3440892 100644 --- a/src/cli/filter_arg.zig +++ b/src/cli/filter_arg.zig @@ -53,7 +53,7 @@ pub fn getCandidatePackagePatterns(allocator: std.mem.Allocator, log: *bun.logge log.errors = 0; log.warnings = 0; - const json_source = switch (bun.sys.File.toSource(json_path, allocator)) { + const json_source = switch (bun.sys.File.toSource(json_path, allocator, .{})) { .err => |err| { switch (err.getErrno()) { .NOENT, .ACCES, .PERM => continue, diff --git a/src/cli/pack_command.zig b/src/cli/pack_command.zig index 2a7d1587d1..3518d52956 100644 --- a/src/cli/pack_command.zig +++ b/src/cli/pack_command.zig @@ -706,7 +706,7 @@ pub const PackCommand = struct { if (strings.eqlComptime(entry_name, "package.json")) { if (entry.kind != .file) break :root_depth; // find more dependencies to bundle - const source = File.toSourceAt(dir, entryNameZ(entry_name, entry_subpath), ctx.allocator).unwrap() catch |err| { + const source = File.toSourceAt(dir, entryNameZ(entry_name, entry_subpath), ctx.allocator, .{}).unwrap() catch |err| { Output.err(err, "failed to read package.json: \"{s}\"", .{entry_subpath}); Global.crash(); }; diff --git a/src/ini.zig b/src/ini.zig index 008dcc7c63..fb2ca0213c 100644 --- a/src/ini.zig +++ b/src/ini.zig @@ -875,7 +875,7 @@ pub fn loadNpmrcConfig( } for (npmrc_paths) |npmrc_path| { - const source = bun.sys.File.toSource(npmrc_path, allocator).unwrap() catch |err| { + const source = bun.sys.File.toSource(npmrc_path, allocator, .{ .convert_bom = true }).unwrap() catch |err| { if (auto_loaded) continue; Output.err(err, "failed to read .npmrc: \"{s}\"", .{npmrc_path}); Global.crash(); diff --git a/src/install/install.zig b/src/install/install.zig index 5511ed1c4f..389fdeea70 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -2958,7 +2958,7 @@ pub const PackageManager = struct { const key = allocator.dupeZ(u8, path) catch bun.outOfMemory(); entry.key_ptr.* = key; - const source = bun.sys.File.toSource(key, allocator).unwrap() catch |err| { + const source = bun.sys.File.toSource(key, allocator, .{}).unwrap() catch |err| { _ = this.map.remove(key); allocator.free(key); return .{ .read_err = err }; @@ -9323,7 +9323,7 @@ pub const PackageManager = struct { // Step 1. parse the nearest package.json file { - const package_json_source = bun.sys.File.toSource(manager.original_package_json_path, ctx.allocator).unwrap() catch |err| { + const package_json_source = bun.sys.File.toSource(manager.original_package_json_path, ctx.allocator, .{}).unwrap() catch |err| { Output.errGeneric("failed to read \"{s}\" for linking: {s}", .{ manager.original_package_json_path, @errorName(err) }); Global.crash(); }; @@ -9504,7 +9504,7 @@ pub const PackageManager = struct { // Step 1. parse the nearest package.json file { - const package_json_source = bun.sys.File.toSource(manager.original_package_json_path, ctx.allocator).unwrap() catch |err| { + const package_json_source = bun.sys.File.toSource(manager.original_package_json_path, ctx.allocator, .{}).unwrap() catch |err| { Output.errGeneric("failed to read \"{s}\" for unlinking: {s}", .{ manager.original_package_json_path, @errorName(err) }); Global.crash(); }; @@ -11484,7 +11484,7 @@ pub const PackageManager = struct { const package_json_source: logger.Source = src: { const package_json_path = bun.path.joinZ(&[_][]const u8{ argument, "package.json" }, .auto); - switch (bun.sys.File.toSource(package_json_path, manager.allocator)) { + switch (bun.sys.File.toSource(package_json_path, manager.allocator, .{})) { .result => |s| break :src s, .err => |e| { Output.err(e, "failed to read {s}", .{bun.fmt.quote(package_json_path)}); @@ -11895,7 +11895,7 @@ pub const PackageManager = struct { const package_json_source: logger.Source = brk: { const package_json_path = bun.path.joinZ(&[_][]const u8{ argument, "package.json" }, .auto); - switch (bun.sys.File.toSource(package_json_path, manager.allocator)) { + switch (bun.sys.File.toSource(package_json_path, manager.allocator, .{})) { .result => |s| break :brk s, .err => |e| { Output.err(e, "failed to read {s}", .{bun.fmt.quote(package_json_path)}); diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index 93ebc1d2c5..5fe6f09495 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -4539,7 +4539,7 @@ pub const Package = extern struct { var local_buf: bun.PathBuffer = undefined; const package_json_path = Path.joinAbsStringBuf(FileSystem.instance.top_level_dir, &local_buf, &.{ path, "package.json" }, .auto); - const source = bun.sys.File.toSource(package_json_path, allocator).unwrap() catch { + const source = bun.sys.File.toSource(package_json_path, allocator, .{}).unwrap() catch { // Can't guarantee this workspace still exists break :brk false; }; @@ -5841,7 +5841,7 @@ pub const Package = extern struct { if (strings.eqlLong(value.name, entry.name, true)) { const note_abs_path = allocator.dupeZ(u8, Path.joinAbsStringZ(cwd, &.{ note_path, "package.json" }, .auto)) catch bun.outOfMemory(); - const note_src = bun.sys.File.toSource(note_abs_path, allocator).unwrap() catch logger.Source.initEmptyFile(note_abs_path); + const note_src = bun.sys.File.toSource(note_abs_path, allocator, .{}).unwrap() catch logger.Source.initEmptyFile(note_abs_path); notes[i] = .{ .text = "Package name is also declared here", @@ -5855,7 +5855,7 @@ pub const Package = extern struct { const abs_path = Path.joinAbsStringZ(cwd, &.{ path, "package.json" }, .auto); - const src = bun.sys.File.toSource(abs_path, allocator).unwrap() catch logger.Source.initEmptyFile(abs_path); + const src = bun.sys.File.toSource(abs_path, allocator, .{}).unwrap() catch logger.Source.initEmptyFile(abs_path); log.addRangeErrorFmtWithNotes( &src, diff --git a/src/install/repository.zig b/src/install/repository.zig index 4a41120cac..cbda94f6cd 100644 --- a/src/install/repository.zig +++ b/src/install/repository.zig @@ -54,17 +54,11 @@ const SloppyGlobalGitConfig = struct { const config_file_path = bun.path.joinAbsStringBufZ(home_dir_path, &config_file_path_buf, &.{".gitconfig"}, .auto); var stack_fallback = std.heap.stackFallback(4096, bun.default_allocator); const allocator = stack_fallback.get(); - var source = File.toSource(config_file_path, allocator).unwrap() catch { + const source = File.toSource(config_file_path, allocator, .{ .convert_bom = true }).unwrap() catch { return; }; defer allocator.free(source.contents); - if (comptime Environment.isWindows) { - if (strings.BOM.detect(source.contents)) |bom| { - source.contents = bom.removeAndConvertToUTF8AndFree(allocator, @constCast(source.contents)) catch bun.outOfMemory(); - } - } - var remaining = bun.strings.split(source.contents, "\n"); var found_askpass = false; var found_ssh_command = false; diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 30fe0042bc..5ff63a9815 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -1255,11 +1255,11 @@ pub fn eqlUtf16(comptime self: string, other: []const u16) bool { return bun.C.memcmp(bun.cast([*]const u8, self.ptr), bun.cast([*]const u8, other.ptr), self.len * @sizeOf(u16)) == 0; } -pub fn toUTF8Alloc(allocator: std.mem.Allocator, js: []const u16) ![]u8 { +pub fn toUTF8Alloc(allocator: std.mem.Allocator, js: []const u16) OOM![]u8 { return try toUTF8AllocWithType(allocator, []const u16, js); } -pub fn toUTF8AllocZ(allocator: std.mem.Allocator, js: []const u16) ![:0]u8 { +pub fn toUTF8AllocZ(allocator: std.mem.Allocator, js: []const u16) OOM![:0]u8 { var list = std.ArrayList(u8).init(allocator); try toUTF8AppendToList(&list, js); try list.append(0); @@ -1451,7 +1451,7 @@ pub const BOM = enum { /// If an allocation is needed, free the input and the caller will /// replace it with the new return - pub fn removeAndConvertToUTF8AndFree(bom: BOM, allocator: std.mem.Allocator, bytes: []u8) ![]u8 { + pub fn removeAndConvertToUTF8AndFree(bom: BOM, allocator: std.mem.Allocator, bytes: []u8) OOM![]u8 { switch (bom) { .utf8 => { _ = bun.c.memmove(bytes.ptr, bytes.ptr + utf8_bytes.len, bytes.len - utf8_bytes.len); @@ -2218,7 +2218,7 @@ pub fn toUTF8AllocWithTypeWithoutInvalidSurrogatePairs(allocator: std.mem.Alloca return list.items; } -pub fn toUTF8AllocWithType(allocator: std.mem.Allocator, comptime Type: type, utf16: Type) ![]u8 { +pub fn toUTF8AllocWithType(allocator: std.mem.Allocator, comptime Type: type, utf16: Type) OOM![]u8 { if (bun.FeatureFlags.use_simdutf and comptime Type == []const u16) { const length = bun.simdutf.length.utf8.from.utf16.le(utf16); // add 16 bytes of padding for SIMDUTF diff --git a/src/sys.zig b/src/sys.zig index 6d40748f62..dab7397648 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -4139,7 +4139,7 @@ pub const File = struct { /// 2. Open a file for reading /// 2. Read the file to a buffer /// 3. Return the File handle and the buffer - pub fn readFromUserInput(dir_fd: anytype, input_path: anytype, allocator: std.mem.Allocator) Maybe([:0]u8) { + pub fn readFromUserInput(dir_fd: anytype, input_path: anytype, allocator: std.mem.Allocator) Maybe([]u8) { var buf: bun.PathBuffer = undefined; const normalized = bun.path.joinAbsStringBufZ( bun.fs.FileSystem.instance.top_level_dir, @@ -4153,7 +4153,7 @@ pub const File = struct { /// 1. Open a file for reading /// 2. Read the file to a buffer /// 3. Return the File handle and the buffer - pub fn readFileFrom(dir_fd: anytype, path: anytype, allocator: std.mem.Allocator) Maybe(struct { File, [:0]u8 }) { + pub fn readFileFrom(dir_fd: anytype, path: anytype, allocator: std.mem.Allocator) Maybe(struct { File, []u8 }) { const ElementType = std.meta.Elem(@TypeOf(path)); const rc = brk: { @@ -4187,16 +4187,14 @@ pub const File = struct { return .{ .result = .{ this, @ptrCast(@constCast("")) } }; } - result.bytes.append(0) catch bun.outOfMemory(); - - return .{ .result = .{ this, result.bytes.items[0 .. result.bytes.items.len - 1 :0] } }; + return .{ .result = .{ this, result.bytes.items } }; } /// 1. Open a file for reading relative to a directory /// 2. Read the file to a buffer /// 3. Close the file /// 4. Return the buffer - pub fn readFrom(dir_fd: anytype, path: anytype, allocator: std.mem.Allocator) Maybe([:0]u8) { + pub fn readFrom(dir_fd: anytype, path: anytype, allocator: std.mem.Allocator) Maybe([]u8) { const file, const bytes = switch (readFileFrom(dir_fd, path, allocator)) { .err => |err| return .{ .err = err }, .result => |result| result, @@ -4206,15 +4204,27 @@ pub const File = struct { return .{ .result = bytes }; } - pub fn toSourceAt(dir_fd: anytype, path: anytype, allocator: std.mem.Allocator) Maybe(bun.logger.Source) { - return switch (readFrom(dir_fd, path, allocator)) { - .err => |err| .{ .err = err }, - .result => |bytes| .{ .result = bun.logger.Source.initPathString(path, bytes) }, + const ToSourceOptions = struct { + convert_bom: bool = false, + }; + + pub fn toSourceAt(dir_fd: anytype, path: anytype, allocator: std.mem.Allocator, opts: ToSourceOptions) Maybe(bun.logger.Source) { + var bytes = switch (readFrom(dir_fd, path, allocator)) { + .err => |err| return .{ .err = err }, + .result => |bytes| bytes, }; + + if (opts.convert_bom) { + if (bun.strings.BOM.detect(bytes)) |bom| { + bytes = bom.removeAndConvertToUTF8AndFree(allocator, bytes) catch bun.outOfMemory(); + } + } + + return .{ .result = bun.logger.Source.initPathString(path, bytes) }; } - pub fn toSource(path: anytype, allocator: std.mem.Allocator) Maybe(bun.logger.Source) { - return toSourceAt(std.fs.cwd(), path, allocator); + pub fn toSource(path: anytype, allocator: std.mem.Allocator, opts: ToSourceOptions) Maybe(bun.logger.Source) { + return toSourceAt(std.fs.cwd(), path, allocator, opts); } }; diff --git a/src/transpiler.zig b/src/transpiler.zig index 5435ab5b21..ea19557d16 100644 --- a/src/transpiler.zig +++ b/src/transpiler.zig @@ -1209,7 +1209,7 @@ pub const Transpiler = struct { var path_buf2: bun.PathBuffer = undefined; @memcpy(path_buf2[0..path.text.len], path.text); path_buf2[path.text.len..][0..bun.bytecode_extension.len].* = bun.bytecode_extension.*; - const bytecode = bun.sys.File.toSourceAt(dirname_fd.unwrapValid() orelse bun.FD.cwd(), path_buf2[0 .. path.text.len + bun.bytecode_extension.len], bun.default_allocator).asValue() orelse break :brk default_value; + const bytecode = bun.sys.File.toSourceAt(dirname_fd.unwrapValid() orelse bun.FD.cwd(), path_buf2[0 .. path.text.len + bun.bytecode_extension.len], bun.default_allocator, .{}).asValue() orelse break :brk default_value; if (bytecode.contents.len == 0) { break :brk default_value; } diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index ee76b33cda..228208f408 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -36,7 +36,7 @@ expect.extend({ toMatchNodeModulesAt, }); -var verdaccio: VerdaccioRegistry; +var registry: VerdaccioRegistry; var port: number; var packageDir: string; /** packageJson = join(packageDir, "package.json"); */ @@ -46,18 +46,18 @@ let users: Record = {}; beforeAll(async () => { setDefaultTimeout(1000 * 60 * 5); - verdaccio = new VerdaccioRegistry(); - port = verdaccio.port; - await verdaccio.start(); + registry = new VerdaccioRegistry(); + port = registry.port; + await registry.start(); }); afterAll(async () => { await Bun.$`rm -f ${import.meta.dir}/htpasswd`.throws(false); - verdaccio.stop(); + registry.stop(); }); beforeEach(async () => { - ({ packageDir, packageJson } = await verdaccio.createTestDir({ saveTextLockfile: false })); + ({ packageDir, packageJson } = await registry.createTestDir({ saveTextLockfile: false })); await Bun.$`rm -f ${import.meta.dir}/htpasswd`.throws(false); await Bun.$`rm -rf ${import.meta.dir}/packages/private-pkg-dont-touch`.throws(false); users = {}; @@ -66,7 +66,7 @@ beforeEach(async () => { }); function registryUrl() { - return verdaccio.registryUrl(); + return registry.registryUrl(); } /** @@ -100,385 +100,6 @@ async function generateRegistryUser(username: string, password: string): Promise } } -describe("npmrc", async () => { - const isBase64Encoded = (opt: string) => opt === "_auth" || opt === "_password"; - - it("works with empty file", async () => { - console.log("package dir", packageDir); - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - const ini = /* ini */ ``; - - await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; - await Bun.$`echo ${JSON.stringify({ - name: "foo", - dependencies: {}, - })} > package.json`.cwd(packageDir); - await Bun.$`${bunExe()} install`.cwd(packageDir).throws(true); - }); - - it("sets default registry", async () => { - console.log("package dir", packageDir); - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - const ini = /* ini */ ` -registry = http://localhost:${port}/ -`; - - await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; - await Bun.$`echo ${JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - }, - })} > package.json`.cwd(packageDir); - await Bun.$`${bunExe()} install`.cwd(packageDir).throws(true); - }); - - it("sets scoped registry", async () => { - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - const ini = /* ini */ ` - @types:registry=http://localhost:${port}/ - `; - - await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; - await Bun.$`echo ${JSON.stringify({ - name: "foo", - dependencies: { - "@types/no-deps": "1.0.0", - }, - })} > package.json`.cwd(packageDir); - await Bun.$`${bunExe()} install`.cwd(packageDir).throws(true); - }); - - it("works with home config", async () => { - console.log("package dir", packageDir); - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - const homeDir = `${packageDir}/home_dir`; - await Bun.$`mkdir -p ${homeDir}`; - console.log("home dir", homeDir); - - const ini = /* ini */ ` - registry=http://localhost:${port}/ - `; - - await Bun.$`echo ${ini} > ${homeDir}/.npmrc`; - await Bun.$`echo ${JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - }, - })} > package.json`.cwd(packageDir); - await Bun.$`${bunExe()} install` - .env({ - ...process.env, - XDG_CONFIG_HOME: `${homeDir}`, - }) - .cwd(packageDir) - .throws(true); - }); - - it("works with two configs", async () => { - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - console.log("package dir", packageDir); - const packageIni = /* ini */ ` - @types:registry=http://localhost:${port}/ - `; - await Bun.$`echo ${packageIni} > ${packageDir}/.npmrc`; - - const homeDir = `${packageDir}/home_dir`; - await Bun.$`mkdir -p ${homeDir}`; - console.log("home dir", homeDir); - const homeIni = /* ini */ ` - registry = http://localhost:${port}/ - `; - await Bun.$`echo ${homeIni} > ${homeDir}/.npmrc`; - - await Bun.$`echo ${JSON.stringify({ - name: "foo", - dependencies: { - "no-deps": "1.0.0", - "@types/no-deps": "1.0.0", - }, - })} > package.json`.cwd(packageDir); - await Bun.$`${bunExe()} install` - .env({ - ...process.env, - XDG_CONFIG_HOME: `${homeDir}`, - }) - .cwd(packageDir) - .throws(true); - }); - - it("package config overrides home config", async () => { - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - console.log("package dir", packageDir); - const packageIni = /* ini */ ` - @types:registry=http://localhost:${port}/ - `; - await Bun.$`echo ${packageIni} > ${packageDir}/.npmrc`; - - const homeDir = `${packageDir}/home_dir`; - await Bun.$`mkdir -p ${homeDir}`; - console.log("home dir", homeDir); - const homeIni = /* ini */ "@types:registry=https://registry.npmjs.org/"; - await Bun.$`echo ${homeIni} > ${homeDir}/.npmrc`; - - await Bun.$`echo ${JSON.stringify({ - name: "foo", - dependencies: { - "@types/no-deps": "1.0.0", - }, - })} > package.json`.cwd(packageDir); - await Bun.$`${bunExe()} install` - .env({ - ...process.env, - XDG_CONFIG_HOME: `${homeDir}`, - }) - .cwd(packageDir) - .throws(true); - }); - - it("default registry from env variable", async () => { - const ini = /* ini */ ` -registry=\${LOL} - `; - - const result = loadNpmrc(ini, { LOL: `http://localhost:${port}/` }); - - expect(result.default_registry_url).toBe(`http://localhost:${port}/`); - }); - - it("default registry from env variable 2", async () => { - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - const ini = /* ini */ ` -registry=http://localhost:\${PORT}/ - `; - - const result = loadNpmrc(ini, { ...env, PORT: port }); - - expect(result.default_registry_url).toEqual(`http://localhost:${port}/`); - }); - - async function makeTest( - options: [option: string, value: string][], - check: (result: { - default_registry_url: string; - default_registry_token: string; - default_registry_username: string; - default_registry_password: string; - }) => void, - ) { - const optionName = await Promise.all(options.map(async ([name, val]) => `${name} = ${val}`)); - test(optionName.join(" "), async () => { - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - const iniInner = await Promise.all( - options.map(async ([option, value]) => { - let finalValue = value; - finalValue = isBase64Encoded(option) ? Buffer.from(finalValue).toString("base64") : finalValue; - return `//registry.npmjs.org/:${option}=${finalValue}`; - }), - ); - - const ini = /* ini */ ` -${iniInner.join("\n")} -`; - - await Bun.$`echo ${JSON.stringify({ - name: "hello", - main: "index.js", - version: "1.0.0", - dependencies: { - "is-even": "1.0.0", - }, - })} > package.json`.cwd(packageDir); - - await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; - - const result = loadNpmrc(ini); - - check(result); - }); - } - - await makeTest([["_authToken", "skibidi"]], result => { - expect(result.default_registry_url).toEqual("https://registry.npmjs.org/"); - expect(result.default_registry_token).toEqual("skibidi"); - }); - - await makeTest( - [ - ["username", "zorp"], - ["_password", "skibidi"], - ], - result => { - expect(result.default_registry_url).toEqual("https://registry.npmjs.org/"); - expect(result.default_registry_username).toEqual("zorp"); - expect(result.default_registry_password).toEqual("skibidi"); - }, - ); - - it("authentication works", async () => { - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - const ini = /* ini */ ` -registry = http://localhost:${port}/ -@needs-auth:registry=http://localhost:${port}/ -//localhost:${port}/:_authToken=${await generateRegistryUser("bilbo_swaggins", "verysecure")} -`; - - await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; - await Bun.$`echo ${JSON.stringify({ - name: "hi", - main: "index.js", - version: "1.0.0", - dependencies: { - "no-deps": "1.0.0", - "@needs-auth/test-pkg": "1.0.0", - }, - "publishConfig": { - "registry": `http://localhost:${port}`, - }, - })} > package.json`.cwd(packageDir); - - await Bun.$`${bunExe()} install`.env(env).cwd(packageDir).throws(true); - }); - - type EnvMap = - | Omit< - { - [key: string]: string; - }, - "dotEnv" - > - | { dotEnv?: Record }; - - function registryConfigOptionTest( - name: string, - _opts: Record | (() => Promise>), - _env?: EnvMap | (() => Promise), - check?: (stdout: string, stderr: string) => void, - ) { - it(`sets scoped registry option: ${name}`, async () => { - console.log("PACKAGE DIR", packageDir); - await Bun.$`rm -rf ${packageDir}/bunfig.toml`; - - const { dotEnv, ...restOfEnv } = _env - ? typeof _env === "function" - ? await _env() - : _env - : { dotEnv: undefined }; - const opts = _opts ? (typeof _opts === "function" ? await _opts() : _opts) : {}; - const dotEnvInner = dotEnv - ? Object.entries(dotEnv) - .map(([k, v]) => `${k}=${k.includes("SECRET_") ? Buffer.from(v).toString("base64") : v}`) - .join("\n") - : ""; - - const ini = ` -registry = http://localhost:${port}/ -${Object.keys(opts) - .map( - k => - `//localhost:${port}/:${k}=${isBase64Encoded(k) && !opts[k].includes("${") ? Buffer.from(opts[k]).toString("base64") : opts[k]}`, - ) - .join("\n")} -`; - - if (dotEnvInner.length > 0) await Bun.$`echo ${dotEnvInner} > ${packageDir}/.env`; - await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; - await Bun.$`echo ${JSON.stringify({ - name: "hi", - main: "index.js", - version: "1.0.0", - dependencies: { - "@needs-auth/test-pkg": "1.0.0", - }, - "publishConfig": { - "registry": `http://localhost:${port}`, - }, - })} > package.json`.cwd(packageDir); - - const { stdout, stderr } = await Bun.$`${bunExe()} install` - .env({ ...env, ...restOfEnv }) - .cwd(packageDir) - .throws(check === undefined); - - if (check) check(stdout.toString(), stderr.toString()); - }); - } - - registryConfigOptionTest("_authToken", async () => ({ - "_authToken": await generateRegistryUser("bilbo_baggins", "verysecure"), - })); - registryConfigOptionTest( - "_authToken with env variable value", - async () => ({ _authToken: "${SUPER_SECRET_TOKEN}" }), - async () => ({ SUPER_SECRET_TOKEN: await generateRegistryUser("bilbo_baggins420", "verysecure") }), - ); - registryConfigOptionTest("username and password", async () => { - await generateRegistryUser("gandalf429", "verysecure"); - return { username: "gandalf429", _password: "verysecure" }; - }); - registryConfigOptionTest( - "username and password with env variable password", - async () => { - await generateRegistryUser("gandalf422", "verysecure"); - return { username: "gandalf422", _password: "${SUPER_SECRET_PASSWORD}" }; - }, - { - SUPER_SECRET_PASSWORD: Buffer.from("verysecure").toString("base64"), - }, - ); - registryConfigOptionTest( - "username and password with .env variable password", - async () => { - await generateRegistryUser("gandalf421", "verysecure"); - return { username: "gandalf421", _password: "${SUPER_SECRET_PASSWORD}" }; - }, - { - dotEnv: { SUPER_SECRET_PASSWORD: "verysecure" }, - }, - ); - - registryConfigOptionTest("_auth", async () => { - await generateRegistryUser("linus", "verysecure"); - const _auth = "linus:verysecure"; - return { _auth }; - }); - - registryConfigOptionTest( - "_auth from .env variable", - async () => { - await generateRegistryUser("zack", "verysecure"); - return { _auth: "${SECRET_AUTH}" }; - }, - { - dotEnv: { SECRET_AUTH: "zack:verysecure" }, - }, - ); - - registryConfigOptionTest( - "_auth from .env variable with no value", - async () => { - await generateRegistryUser("zack420", "verysecure"); - return { _auth: "${SECRET_AUTH}" }; - }, - { - dotEnv: { SECRET_AUTH: "" }, - }, - (stdout: string, stderr: string) => { - expect(stderr).toContain("received an empty string"); - }, - ); -}); - describe("auto-install", () => { test("symlinks (and junctions) are created correctly in the install cache", async () => { const { stdout, stderr, exited } = spawn({ @@ -744,7 +365,7 @@ ljelkjwelkgjw;lekj;lkejflkj describe("whoami", async () => { test("can get username", async () => { - const bunfig = await verdaccio.authBunfig("whoami"); + const bunfig = await registry.authBunfig("whoami"); await Promise.all([ write( packageJson, @@ -2148,7 +1769,7 @@ saveTextLockfile = false ), ]); - // first install this package from verdaccio + // first install this package from registry await runBunInstall(env, packageDir); const lockfile = await parseLockfile(packageDir); for (const pkg of Object.values(lockfile.packages) as any) { @@ -8826,7 +8447,7 @@ describe("outdated", () => { }); }); -// TODO: setup verdaccio to run across multiple test files, then move this and a few other describe +// TODO: setup registry to run across multiple test files, then move this and a few other describe // scopes (update, hoisting, ...) to other files // // test/cli/install/registry/bun-install-windowsshim.test.ts: diff --git a/test/cli/install/npmrc.test.ts b/test/cli/install/npmrc.test.ts new file mode 100644 index 0000000000..0d4ca771d5 --- /dev/null +++ b/test/cli/install/npmrc.test.ts @@ -0,0 +1,447 @@ +import { write } from "bun"; +import { expect, test, it, beforeAll, afterAll, describe } from "bun:test"; +import { VerdaccioRegistry, bunExe, bunEnv as env, stderrForInstall } from "harness"; +import { join } from "path"; +import { rm } from "fs/promises"; +const { iniInternals } = require("bun:internal-for-testing"); +const { loadNpmrc } = iniInternals; + +var registry = new VerdaccioRegistry(); + +beforeAll(async () => { + await registry.start(); +}); + +afterAll(() => { + registry.stop(); +}); + +describe("npmrc", async () => { + const isBase64Encoded = (opt: string) => opt === "_auth" || opt === "_password"; + + it("should convert to utf8 if BOM", async () => { + const { packageDir, packageJson } = await registry.createTestDir(); + + await Promise.all([ + write(join(packageDir, ".npmrc"), Buffer.from(`\ufeff\ncache=hi!`, "utf16le")), + write(packageJson, JSON.stringify({ name: "foo", version: "1.0.0" })), + rm(join(packageDir, "bunfig.toml"), { force: true }), + ]); + + const originalCacheDir = env.BUN_INSTALL_CACHE_DIR; + delete env.BUN_INSTALL_CACHE_DIR; + const { stdout, stderr, exited } = Bun.spawn({ + cmd: [bunExe(), "pm", "cache"], + cwd: packageDir, + env, + stdout: "pipe", + stderr: "pipe", + }); + env.BUN_INSTALL_CACHE_DIR = originalCacheDir; + + const out = await Bun.readableStreamToText(stdout); + const err = stderrForInstall(await Bun.readableStreamToText(stderr)); + console.log({ out, err }); + expect(err).toBeEmpty(); + expect(out.endsWith("hi!")).toBeTrue(); + + expect(await exited).toBe(0); + }); + + it("works with empty file", async () => { + const { packageDir, packageJson } = await registry.createTestDir(); + + console.log("package dir", packageDir); + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + const ini = /* ini */ ``; + + await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; + await Bun.$`echo ${JSON.stringify({ + name: "foo", + dependencies: {}, + })} > package.json`.cwd(packageDir); + await Bun.$`${bunExe()} install`.cwd(packageDir).throws(true); + }); + + it("sets default registry", async () => { + const { packageDir, packageJson } = await registry.createTestDir(); + + console.log("package dir", packageDir); + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + const ini = /* ini */ ` +registry = http://localhost:${registry.port}/ +`; + + await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; + await Bun.$`echo ${JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + }, + })} > package.json`.cwd(packageDir); + await Bun.$`${bunExe()} install`.cwd(packageDir).throws(true); + }); + + it("sets scoped registry", async () => { + const { packageDir, packageJson } = await registry.createTestDir(); + + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + const ini = /* ini */ ` + @types:registry=http://localhost:${registry.port}/ + `; + + await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; + await Bun.$`echo ${JSON.stringify({ + name: "foo", + dependencies: { + "@types/no-deps": "1.0.0", + }, + })} > package.json`.cwd(packageDir); + await Bun.$`${bunExe()} install`.cwd(packageDir).throws(true); + }); + + it("works with home config", async () => { + const { packageDir, packageJson } = await registry.createTestDir(); + + console.log("package dir", packageDir); + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + const homeDir = `${packageDir}/home_dir`; + await Bun.$`mkdir -p ${homeDir}`; + console.log("home dir", homeDir); + + const ini = /* ini */ ` + registry=http://localhost:${registry.port}/ + `; + + await Bun.$`echo ${ini} > ${homeDir}/.npmrc`; + await Bun.$`echo ${JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + }, + })} > package.json`.cwd(packageDir); + await Bun.$`${bunExe()} install` + .env({ + ...process.env, + XDG_CONFIG_HOME: `${homeDir}`, + }) + .cwd(packageDir) + .throws(true); + }); + + it("works with two configs", async () => { + const { packageDir, packageJson } = await registry.createTestDir(); + + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + console.log("package dir", packageDir); + const packageIni = /* ini */ ` + @types:registry=http://localhost:${registry.port}/ + `; + await Bun.$`echo ${packageIni} > ${packageDir}/.npmrc`; + + const homeDir = `${packageDir}/home_dir`; + await Bun.$`mkdir -p ${homeDir}`; + console.log("home dir", homeDir); + const homeIni = /* ini */ ` + registry = http://localhost:${registry.port}/ + `; + await Bun.$`echo ${homeIni} > ${homeDir}/.npmrc`; + + await Bun.$`echo ${JSON.stringify({ + name: "foo", + dependencies: { + "no-deps": "1.0.0", + "@types/no-deps": "1.0.0", + }, + })} > package.json`.cwd(packageDir); + await Bun.$`${bunExe()} install` + .env({ + ...process.env, + XDG_CONFIG_HOME: `${homeDir}`, + }) + .cwd(packageDir) + .throws(true); + }); + + it("package config overrides home config", async () => { + const { packageDir, packageJson } = await registry.createTestDir(); + + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + console.log("package dir", packageDir); + const packageIni = /* ini */ ` + @types:registry=http://localhost:${registry.port}/ + `; + await Bun.$`echo ${packageIni} > ${packageDir}/.npmrc`; + + const homeDir = `${packageDir}/home_dir`; + await Bun.$`mkdir -p ${homeDir}`; + console.log("home dir", homeDir); + const homeIni = /* ini */ "@types:registry=https://registry.npmjs.org/"; + await Bun.$`echo ${homeIni} > ${homeDir}/.npmrc`; + + await Bun.$`echo ${JSON.stringify({ + name: "foo", + dependencies: { + "@types/no-deps": "1.0.0", + }, + })} > package.json`.cwd(packageDir); + await Bun.$`${bunExe()} install` + .env({ + ...process.env, + XDG_CONFIG_HOME: `${homeDir}`, + }) + .cwd(packageDir) + .throws(true); + }); + + it("default registry from env variable", async () => { + const { packageDir, packageJson } = await registry.createTestDir(); + + const ini = /* ini */ ` +registry=\${LOL} + `; + + const result = loadNpmrc(ini, { LOL: `http://localhost:${registry.port}/` }); + + expect(result.default_registry_url).toBe(`http://localhost:${registry.port}/`); + }); + + it("default registry from env variable 2", async () => { + const { packageDir, packageJson } = await registry.createTestDir(); + + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + const ini = /* ini */ ` +registry=http://localhost:\${PORT}/ + `; + + const result = loadNpmrc(ini, { ...env, PORT: registry.port }); + + expect(result.default_registry_url).toEqual(`http://localhost:${registry.port}/`); + }); + + async function makeTest( + options: [option: string, value: string][], + check: (result: { + default_registry_url: string; + default_registry_token: string; + default_registry_username: string; + default_registry_password: string; + }) => void, + ) { + const optionName = await Promise.all(options.map(async ([name, val]) => `${name} = ${val}`)); + test(optionName.join(" "), async () => { + const { packageDir, packageJson } = await registry.createTestDir(); + + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + const iniInner = await Promise.all( + options.map(async ([option, value]) => { + let finalValue = value; + finalValue = isBase64Encoded(option) ? Buffer.from(finalValue).toString("base64") : finalValue; + return `//registry.npmjs.org/:${option}=${finalValue}`; + }), + ); + + const ini = /* ini */ ` +${iniInner.join("\n")} +`; + + await Bun.$`echo ${JSON.stringify({ + name: "hello", + main: "index.js", + version: "1.0.0", + dependencies: { + "is-even": "1.0.0", + }, + })} > package.json`.cwd(packageDir); + + await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; + + const result = loadNpmrc(ini); + + check(result); + }); + } + + await makeTest([["_authToken", "skibidi"]], result => { + expect(result.default_registry_url).toEqual("https://registry.npmjs.org/"); + expect(result.default_registry_token).toEqual("skibidi"); + }); + + await makeTest( + [ + ["username", "zorp"], + ["_password", "skibidi"], + ], + result => { + expect(result.default_registry_url).toEqual("https://registry.npmjs.org/"); + expect(result.default_registry_username).toEqual("zorp"); + expect(result.default_registry_password).toEqual("skibidi"); + }, + ); + + it("authentication works", async () => { + const { packageDir, packageJson } = await registry.createTestDir(); + + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + const ini = /* ini */ ` +registry = http://localhost:${registry.port}/ +@needs-auth:registry=http://localhost:${registry.port}/ +//localhost:${registry.port}/:_authToken=${await registry.generateUser("bilbo_swaggins", "verysecure")} +`; + + await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; + await Bun.$`echo ${JSON.stringify({ + name: "hi", + main: "index.js", + version: "1.0.0", + dependencies: { + "no-deps": "1.0.0", + "@needs-auth/test-pkg": "1.0.0", + }, + "publishConfig": { + "registry": `http://localhost:${registry.port}`, + }, + })} > package.json`.cwd(packageDir); + + await Bun.$`${bunExe()} install`.env(env).cwd(packageDir).throws(true); + }); + + type EnvMap = + | Omit< + { + [key: string]: string; + }, + "dotEnv" + > + | { dotEnv?: Record }; + + function registryConfigOptionTest( + name: string, + _opts: Record | (() => Promise>), + _env?: EnvMap | (() => Promise), + check?: (stdout: string, stderr: string) => void, + ) { + it(`sets scoped registry option: ${name}`, async () => { + const { packageDir, packageJson } = await registry.createTestDir(); + + console.log("PACKAGE DIR", packageDir); + await Bun.$`rm -rf ${packageDir}/bunfig.toml`; + + const { dotEnv, ...restOfEnv } = _env + ? typeof _env === "function" + ? await _env() + : _env + : { dotEnv: undefined }; + const opts = _opts ? (typeof _opts === "function" ? await _opts() : _opts) : {}; + const dotEnvInner = dotEnv + ? Object.entries(dotEnv) + .map(([k, v]) => `${k}=${k.includes("SECRET_") ? Buffer.from(v).toString("base64") : v}`) + .join("\n") + : ""; + + const ini = ` +registry = http://localhost:${registry.port}/ +${Object.keys(opts) + .map( + k => + `//localhost:${registry.port}/:${k}=${isBase64Encoded(k) && !opts[k].includes("${") ? Buffer.from(opts[k]).toString("base64") : opts[k]}`, + ) + .join("\n")} +`; + + if (dotEnvInner.length > 0) await Bun.$`echo ${dotEnvInner} > ${packageDir}/.env`; + await Bun.$`echo ${ini} > ${packageDir}/.npmrc`; + await Bun.$`echo ${JSON.stringify({ + name: "hi", + main: "index.js", + version: "1.0.0", + dependencies: { + "@needs-auth/test-pkg": "1.0.0", + }, + "publishConfig": { + "registry": `http://localhost:${registry.port}`, + }, + })} > package.json`.cwd(packageDir); + + const { stdout, stderr } = await Bun.$`${bunExe()} install` + .env({ ...env, ...restOfEnv }) + .cwd(packageDir) + .throws(check === undefined); + + if (check) check(stdout.toString(), stderr.toString()); + }); + } + + registryConfigOptionTest("_authToken", async () => ({ + "_authToken": await registry.generateUser("bilbo_baggins", "verysecure"), + })); + registryConfigOptionTest( + "_authToken with env variable value", + async () => ({ _authToken: "${SUPER_SECRET_TOKEN}" }), + async () => ({ SUPER_SECRET_TOKEN: await registry.generateUser("bilbo_baggins420", "verysecure") }), + ); + registryConfigOptionTest("username and password", async () => { + await registry.generateUser("gandalf429", "verysecure"); + return { username: "gandalf429", _password: "verysecure" }; + }); + registryConfigOptionTest( + "username and password with env variable password", + async () => { + await registry.generateUser("gandalf422", "verysecure"); + return { username: "gandalf422", _password: "${SUPER_SECRET_PASSWORD}" }; + }, + { + SUPER_SECRET_PASSWORD: Buffer.from("verysecure").toString("base64"), + }, + ); + registryConfigOptionTest( + "username and password with .env variable password", + async () => { + await registry.generateUser("gandalf421", "verysecure"); + return { username: "gandalf421", _password: "${SUPER_SECRET_PASSWORD}" }; + }, + { + dotEnv: { SUPER_SECRET_PASSWORD: "verysecure" }, + }, + ); + + registryConfigOptionTest("_auth", async () => { + await registry.generateUser("linus", "verysecure"); + const _auth = "linus:verysecure"; + return { _auth }; + }); + + registryConfigOptionTest( + "_auth from .env variable", + async () => { + await registry.generateUser("zack", "verysecure"); + return { _auth: "${SECRET_AUTH}" }; + }, + { + dotEnv: { SECRET_AUTH: "zack:verysecure" }, + }, + ); + + registryConfigOptionTest( + "_auth from .env variable with no value", + async () => { + await registry.generateUser("zack420", "verysecure"); + return { _auth: "${SECRET_AUTH}" }; + }, + { + dotEnv: { SECRET_AUTH: "" }, + }, + (stdout: string, stderr: string) => { + expect(stderr).toContain("received an empty string"); + }, + ); +});