diff options
author | 2023-01-20 02:24:56 +0200 | |
---|---|---|
committer | 2023-01-19 16:24:56 -0800 | |
commit | 9dfbf57397fc79fcff76da90640cd580517f742e (patch) | |
tree | b842848272266038f1efcb13df730c4852a079ae | |
parent | e04fe64a826fa0642e9fa91faaae898d488f8156 (diff) | |
download | bun-9dfbf57397fc79fcff76da90640cd580517f742e.tar.gz bun-9dfbf57397fc79fcff76da90640cd580517f742e.tar.zst bun-9dfbf57397fc79fcff76da90640cd580517f742e.zip |
repopulate `alias_map` correctly (#1847)
-rw-r--r-- | src/install/install.zig | 8 | ||||
-rw-r--r-- | src/install/lockfile.zig | 50 | ||||
-rw-r--r-- | test/bun.js/install/basic.toml | 1 | ||||
-rw-r--r-- | test/bun.js/install/bun-install.test.ts | 251 | ||||
-rw-r--r-- | test/bun.js/install/tarball.tgz | bin | 0 -> 190 bytes |
5 files changed, 240 insertions, 70 deletions
diff --git a/src/install/install.zig b/src/install/install.zig index 674ca5ae0..b131175c8 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -89,9 +89,6 @@ const Dependency = @import("./dependency.zig"); const Behavior = @import("./dependency.zig").Behavior; const FolderResolution = @import("./resolvers/folder_resolver.zig").FolderResolution; -pub const ExternalStringBuilder = StructBuilder.Builder(ExternalString); -pub const SmallExternalStringList = ExternalSlice(String); - pub fn ExternalSlice(comptime Type: type) type { return ExternalSliceAligned(Type, null); } @@ -144,11 +141,6 @@ pub const VersionSlice = ExternalSlice(Semver.Version); pub const ExternalStringMap = extern struct { name: ExternalStringList = ExternalStringList{}, value: ExternalStringList = ExternalStringList{}, - - pub const Small = extern struct { - name: SmallExternalStringList = SmallExternalStringList{}, - value: SmallExternalStringList = SmallExternalStringList{}, - }; }; pub const PackageNameHash = u64; diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index be56f56f1..c3a6f5d71 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -85,8 +85,8 @@ const zero_hash = std.mem.zeroes(MetaHash); const PackageJSON = @import("../resolver/package_json.zig").PackageJSON; -pub const ExternalStringBuilder = StructBuilder.Builder(ExternalString); -pub const SmallExternalStringList = ExternalSlice(String); +const AliasMap = std.ArrayHashMapUnmanaged(PackageID, String, ArrayIdentityContext, false); +const NameHashMap = std.ArrayHashMapUnmanaged(u32, String, ArrayIdentityContext, false); // Serialized data /// The version of the lockfile format, intended to prevent data corruption for format changes. @@ -106,8 +106,8 @@ allocator: std.mem.Allocator, scratch: Scratch = Scratch{}, scripts: Scripts = .{}, -alias_map: std.ArrayHashMapUnmanaged(PackageID, String, ArrayIdentityContext, false) = .{}, -workspace_paths: std.ArrayHashMapUnmanaged(u32, String, ArrayIdentityContext, false) = .{}, +alias_map: AliasMap = .{}, +workspace_paths: NameHashMap = .{}, const Stream = std.io.FixedBufferStream([]u8); pub const default_filename = "bun.lockb"; @@ -3157,21 +3157,21 @@ pub fn deinit(this: *Lockfile) void { } const Buffers = struct { - trees: Tree.List = Tree.List{}, - hoisted_packages: PackageIDList = PackageIDList{}, - resolutions: PackageIDList = PackageIDList{}, - dependencies: DependencyList = DependencyList{}, - extern_strings: ExternalStringBuffer = ExternalStringBuffer{}, + trees: Tree.List = .{}, + hoisted_packages: PackageIDList = .{}, + resolutions: PackageIDList = .{}, + dependencies: DependencyList = .{}, + extern_strings: ExternalStringBuffer = .{}, // node_modules_folders: NodeModulesFolderList = NodeModulesFolderList{}, // node_modules_package_ids: PackageIDList = PackageIDList{}, - string_bytes: StringBuffer = StringBuffer{}, + string_bytes: StringBuffer = .{}, pub fn deinit(this: *Buffers, allocator: std.mem.Allocator) void { - try this.trees.deinit(allocator); - try this.resolutions.deinit(allocator); - try this.dependencies.deinit(allocator); - try this.extern_strings.deinit(allocator); - try this.string_bytes.deinit(allocator); + this.trees.deinit(allocator); + this.resolutions.deinit(allocator); + this.dependencies.deinit(allocator); + this.extern_strings.deinit(allocator); + this.string_bytes.deinit(allocator); } pub fn preallocate(this: *Buffers, that: Buffers, allocator: std.mem.Allocator) !void { @@ -3337,7 +3337,7 @@ const Buffers = struct { } } - pub fn load(stream: *Stream, allocator: std.mem.Allocator, log: *logger.Log) !Buffers { + pub fn load(stream: *Stream, allocator: std.mem.Allocator, log: *logger.Log, alias_map: *AliasMap) !Buffers { var this = Buffers{}; var external_dependency_list_: std.ArrayListUnmanaged(Dependency.External) = std.ArrayListUnmanaged(Dependency.External){}; @@ -3380,16 +3380,26 @@ const Buffers = struct { // Dependencies are serialized separately. // This is unfortunate. However, not using pointers for Semver Range's make the code a lot more complex. this.dependencies = try DependencyList.initCapacity(allocator, external_dependency_list.len); + const string_buf = this.string_bytes.items; const extern_context = Dependency.Context{ .log = log, .allocator = allocator, - .buffer = this.string_bytes.items, + .buffer = string_buf, }; this.dependencies.expandToCapacity(); this.dependencies.items.len = external_dependency_list.len; - for (external_dependency_list) |dep, i| { - this.dependencies.items[i] = Dependency.toDependency(dep, extern_context); + for (external_dependency_list) |external_dep, i| { + const dep = Dependency.toDependency(external_dep, extern_context); + this.dependencies.items[i] = dep; + switch (dep.version.tag) { + .npm => { + if (!dep.name.eql(dep.version.value.npm.name, string_buf, string_buf)) { + try alias_map.put(allocator, this.resolutions.items[i], dep.name); + } + }, + else => {}, + } } return this; @@ -3458,7 +3468,7 @@ pub const Serializer = struct { total_buffer_size, allocator, ); - lockfile.buffers = try Lockfile.Buffers.load(stream, allocator, log); + lockfile.buffers = try Lockfile.Buffers.load(stream, allocator, log, &lockfile.alias_map); if ((try stream.reader().readIntLittle(u64)) != 0) { return error.@"Lockfile is malformed (expected 0 at the end)"; } diff --git a/test/bun.js/install/basic.toml b/test/bun.js/install/basic.toml index 5007a8efd..a8476663a 100644 --- a/test/bun.js/install/basic.toml +++ b/test/bun.js/install/basic.toml @@ -1,4 +1,5 @@ [install] +cache = false registry = "http://localhost:54321/" [install.scopes] diff --git a/test/bun.js/install/bun-install.test.ts b/test/bun.js/install/bun-install.test.ts index 31f4b28d9..cf6d3a1ca 100644 --- a/test/bun.js/install/bun-install.test.ts +++ b/test/bun.js/install/bun-install.test.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun"; +import { file, spawn } from "bun"; import { afterAll, afterEach, @@ -8,7 +8,7 @@ import { it, } from "bun:test"; import { bunExe } from "bunExe"; -import { mkdir, mkdtemp, readdir, readlink, rm, writeFile } from "fs/promises"; +import { access, mkdir, mkdtemp, readdir, readlink, rm, writeFile } from "fs/promises"; import { join } from "path"; import { tmpdir } from "os"; import { bunEnv } from "bunEnv"; @@ -87,6 +87,12 @@ it("should handle missing package", async () => { ]); expect(await exited).toBe(1); expect(requested).toBe(1); + try { + await access(join(package_dir, "bun.lockb")); + expect(() => {}).toThrow(); + } catch (err: any) { + expect(err.code).toBe("ENOENT"); + } }); it("should handle @scoped authentication", async () => { @@ -134,6 +140,12 @@ it("should handle @scoped authentication", async () => { expect(seen_token).toBe(true); expect(await exited).toBe(1); expect(requested).toBe(1); + try { + await access(join(package_dir, "bun.lockb")); + expect(() => {}).toThrow(); + } catch (err: any) { + expect(err.code).toBe("ENOENT"); + } }); it("should handle empty string in dependencies", async () => { @@ -178,6 +190,12 @@ it("should handle empty string in dependencies", async () => { ]); expect(await exited).toBe(1); expect(requested).toBe(1); + try { + await access(join(package_dir, "bun.lockb")); + expect(() => {}).toThrow(); + } catch (err: any) { + expect(err.code).toBe("ENOENT"); + } }); it("should handle workspaces", async () => { @@ -218,11 +236,13 @@ it("should handle workspaces", async () => { expect(await exited).toBe(0); expect(requested).toBe(0); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ + ".cache", "Bar", ]); expect(await readlink(join(package_dir, "node_modules", "Bar"))).toBe( join("..", "bar"), ); + await access(join(package_dir, "bun.lockb")); }); it("should handle inter-dependency between workspaces", async () => { @@ -278,6 +298,7 @@ it("should handle inter-dependency between workspaces", async () => { expect(await exited).toBe(0); expect(requested).toBe(0); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ + ".cache", "Bar", "Baz", ]); @@ -287,6 +308,7 @@ it("should handle inter-dependency between workspaces", async () => { expect(await readlink(join(package_dir, "node_modules", "Baz"))).toBe( join("..", "packages", "baz"), ); + await access(join(package_dir, "bun.lockb")); }); it("should handle inter-dependency between workspaces (devDependencies)", async () => { @@ -342,6 +364,7 @@ it("should handle inter-dependency between workspaces (devDependencies)", async expect(await exited).toBe(0); expect(requested).toBe(0); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ + ".cache", "Bar", "Baz", ]); @@ -351,6 +374,7 @@ it("should handle inter-dependency between workspaces (devDependencies)", async expect(await readlink(join(package_dir, "node_modules", "Baz"))).toBe( join("..", "packages", "baz"), ); + await access(join(package_dir, "bun.lockb")); }); it("should handle inter-dependency between workspaces (optionalDependencies)", async () => { @@ -406,6 +430,7 @@ it("should handle inter-dependency between workspaces (optionalDependencies)", a expect(await exited).toBe(0); expect(requested).toBe(0); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ + ".cache", "Bar", "Baz", ]); @@ -415,6 +440,7 @@ it("should handle inter-dependency between workspaces (optionalDependencies)", a expect(await readlink(join(package_dir, "node_modules", "Baz"))).toBe( join("..", "packages", "baz"), ); + await access(join(package_dir, "bun.lockb")); }); it("should ignore peerDependencies within workspaces", async () => { @@ -461,11 +487,13 @@ it("should ignore peerDependencies within workspaces", async () => { expect(await exited).toBe(0); expect(requested).toBe(0); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ + ".cache", "Baz", ]); expect(await readlink(join(package_dir, "node_modules", "Baz"))).toBe( join("..", "packages", "baz"), ); + await access(join(package_dir, "bun.lockb")); }); it("should handle life-cycle scripts within workspaces", async () => { @@ -522,25 +550,49 @@ it("should handle life-cycle scripts within workspaces", async () => { expect(await exited).toBe(0); expect(requested).toBe(0); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ + ".cache", "Bar", ]); expect(await readlink(join(package_dir, "node_modules", "Bar"))).toBe( join("..", "bar"), ); + await access(join(package_dir, "bun.lockb")); }); -it("should handle dependency aliasing", async () => { - const urls: string[] = []; - handler = async (request) => { +function dummyRegistry(urls) { + return async (request) => { + urls.push(request.url); expect(request.method).toBe("GET"); + if (request.url.endsWith(".tgz")) { + return new Response(file(join(import.meta.dir, "tarball.tgz"))); + } expect(request.headers.get("accept")).toBe( "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*", ); expect(request.headers.get("npm-auth-type")).toBe(null); expect(await request.text()).toBe(""); - urls.push(request.url); - return new Response("not to be found", { status: 404 }); + const name = request.url.slice(request.url.lastIndexOf("/") + 1); + return new Response(JSON.stringify({ + name: name, + versions: { + "0.0.2": { + name: name, + version: "0.0.2", + dist: { + tarball: `${request.url}.tgz`, + }, + }, + }, + "dist-tags": { + latest: "0.0.2", + }, + })); }; +} + +it("should handle dependency aliasing", async () => { + const urls = []; + handler = dummyRegistry(urls); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -561,30 +613,37 @@ it("should handle dependency aliasing", async () => { }); expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); - expect(err).toContain('error: package "baz" not found localhost/baz 404'); - expect(err).toContain("error: Bar@npm:baz failed to resolve"); + expect(err).toContain("Saved lockfile"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([""]); + expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + baz@0.0.2", + "", + " 1 packages installed", + ]); + expect(await exited).toBe(0); expect(urls).toEqual([ "http://localhost:54321/baz", + "http://localhost:54321/baz.tgz", ]); - expect(await exited).toBe(1); - expect(requested).toBe(1); + 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: "baz", + version: "0.0.2", + }) + await access(join(package_dir, "bun.lockb")); }); it("should handle dependency aliasing (versioned)", async () => { const urls: string[] = []; - handler = async (request) => { - expect(request.method).toBe("GET"); - expect(request.headers.get("accept")).toBe( - "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*", - ); - expect(request.headers.get("npm-auth-type")).toBe(null); - expect(await request.text()).toBe(""); - urls.push(request.url); - return new Response("not to be found", { status: 404 }); - }; + handler = dummyRegistry(urls); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -605,30 +664,37 @@ it("should handle dependency aliasing (versioned)", async () => { }); expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); - expect(err).toContain('error: package "baz" not found localhost/baz 404'); - expect(err).toContain("error: Bar@npm:baz@0.0.2 failed to resolve"); + expect(err).toContain("Saved lockfile"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([""]); + expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + baz@0.0.2", + "", + " 1 packages installed", + ]); + expect(await exited).toBe(0); expect(urls).toEqual([ "http://localhost:54321/baz", + "http://localhost:54321/baz.tgz", ]); - expect(await exited).toBe(1); - expect(requested).toBe(1); + 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: "baz", + version: "0.0.2", + }) + await access(join(package_dir, "bun.lockb")); }); it("should handle dependency aliasing (dist-tagged)", async () => { const urls: string[] = []; - handler = async (request) => { - expect(request.method).toBe("GET"); - expect(request.headers.get("accept")).toBe( - "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*", - ); - expect(request.headers.get("npm-auth-type")).toBe(null); - expect(await request.text()).toBe(""); - urls.push(request.url); - return new Response("not to be found", { status: 404 }); - }; + handler = dummyRegistry(urls); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -649,14 +715,115 @@ it("should handle dependency aliasing (dist-tagged)", async () => { }); expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); - expect(err).toContain('error: package "baz" not found localhost/baz 404'); - expect(err).toContain("error: Bar@npm:baz@latest failed to resolve"); + expect(err).toContain("Saved lockfile"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([""]); + expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + baz@0.0.2", + "", + " 1 packages installed", + ]); + expect(await exited).toBe(0); expect(urls).toEqual([ "http://localhost:54321/baz", + "http://localhost:54321/baz.tgz", ]); - expect(await exited).toBe(1); - expect(requested).toBe(1); + 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: "baz", + version: "0.0.2", + }) + await access(join(package_dir, "bun.lockb")); +}); + +it("should not reinstall aliased dependencies", async () => { + const urls = []; + handler = dummyRegistry(urls); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + "Bar": "npm:baz", + }, + }), + ); + const { stdout: stdout1, stderr: stderr1, exited: exited1 } = spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr1).toBeDefined(); + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("Saved lockfile"); + expect(stdout1).toBeDefined(); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + baz@0.0.2", + "", + " 1 packages installed", + ]); + expect(await exited1).toBe(0); + expect(urls).toEqual([ + "http://localhost:54321/baz", + "http://localhost:54321/baz.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: "baz", + version: "0.0.2", + }) + await access(join(package_dir, "bun.lockb")); + // Performs `bun install` again, expects no-op + urls.length = 0; + const { stdout: stdout2, stderr: stderr2, exited: exited2 } = spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr2).toBeDefined(); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("Saved lockfile"); + expect(stdout2).toBeDefined(); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ + "", + "Checked 1 installs across 2 packages (no changes)", + ]); + expect(await exited2).toBe(0); + expect(urls).toEqual([]); + 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: "baz", + version: "0.0.2", + }) + await access(join(package_dir, "bun.lockb")); }); diff --git a/test/bun.js/install/tarball.tgz b/test/bun.js/install/tarball.tgz Binary files differnew file mode 100644 index 000000000..384081343 --- /dev/null +++ b/test/bun.js/install/tarball.tgz |