import { file, write } from "bun"; import { install_test_helpers } from "bun:internal-for-testing"; import { beforeEach, describe, expect, test } from "bun:test"; import { mkdirSync, rmSync, writeFileSync } from "fs"; import { cp } from "fs/promises"; import { bunExe, bunEnv as env, runBunInstall, tmpdirSync, toMatchNodeModulesAt } from "harness"; import { join } from "path"; const { parseLockfile } = install_test_helpers; expect.extend({ toMatchNodeModulesAt }); var testCounter: number = 0; // not necessary, but verdaccio will be added to this file in the near future var port: number = 4873; var packageDir: string; beforeEach(() => { packageDir = tmpdirSync(); env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache"); env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp"); writeFileSync( join(packageDir, "bunfig.toml"), ` [install] cache = false `, ); }); test("dependency on workspace without version in package.json", async () => { await Promise.all([ write( join(packageDir, "package.json"), JSON.stringify({ name: "foo", workspaces: ["packages/*"], }), ), write( join(packageDir, "packages", "mono", "package.json"), JSON.stringify({ name: "lodash", }), ), ]); mkdirSync(join(packageDir, "packages", "bar"), { recursive: true }); const shouldWork: string[] = [ "*", "*.*.*", "=*", "kjwoehcojrgjoj", // dist-tag does not exist, should choose local workspace "*.1.*", "*-pre", ]; const shouldNotWork: string[] = [ "1", "1.*", "1.1.*", "1.1.1", "*-pre+build", "*+build", "latest", // dist-tag exists, should choose package from npm "", ]; for (const version of shouldWork) { writeFileSync( join(packageDir, "packages", "bar", "package.json"), JSON.stringify({ name: "bar", version: "1.0.0", dependencies: { lodash: version, }, }), ); const { out } = await runBunInstall(env, packageDir); const lockfile = parseLockfile(packageDir); expect(lockfile).toMatchNodeModulesAt(packageDir); expect(lockfile).toMatchSnapshot(`version: ${version}`); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "2 packages installed", ]); rmSync(join(packageDir, "node_modules"), { recursive: true, force: true }); rmSync(join(packageDir, "bun.lockb"), { recursive: true, force: true }); } // downloads the package from the registry instead of // using the workspace locally for (const version of shouldNotWork) { writeFileSync( join(packageDir, "packages", "bar", "package.json"), JSON.stringify({ name: "bar", version: "1.0.0", dependencies: { lodash: version, }, }), ); const { out } = await runBunInstall(env, packageDir); const lockfile = parseLockfile(packageDir); expect(lockfile).toMatchNodeModulesAt(packageDir); expect(lockfile).toMatchSnapshot(`version: ${version}`); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "3 packages installed", ]); rmSync(join(packageDir, "node_modules"), { recursive: true, force: true }); rmSync(join(packageDir, "packages", "bar", "node_modules"), { recursive: true, force: true }); rmSync(join(packageDir, "bun.lockb"), { recursive: true, force: true }); } }, 20_000); test("dependency on same name as workspace and dist-tag", async () => { await Promise.all([ write( join(packageDir, "package.json"), JSON.stringify({ name: "foo", workspaces: ["packages/*"], }), ), write( join(packageDir, "packages", "mono", "package.json"), JSON.stringify({ name: "lodash", version: "4.17.21", }), ), write( join(packageDir, "packages", "bar", "package.json"), JSON.stringify({ name: "bar", version: "1.0.0", dependencies: { lodash: "latest", }, }), ), ]); const { out } = await runBunInstall(env, packageDir); const lockfile = parseLockfile(packageDir); expect(lockfile).toMatchSnapshot("with version"); expect(lockfile).toMatchNodeModulesAt(packageDir); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "3 packages installed", ]); }); test("successfully installs workspace when path already exists in node_modules", async () => { await Promise.all([ write( join(packageDir, "package.json"), JSON.stringify({ name: "foo", workspaces: ["pkg1"], }), ), write( join(packageDir, "pkg1", "package.json"), JSON.stringify({ name: "pkg1", }), ), // stale package in node_modules write( join(packageDir, "node_modules", "pkg1", "package.json"), JSON.stringify({ name: "pkg2", }), ), ]); await runBunInstall(env, packageDir); expect(await file(join(packageDir, "node_modules", "pkg1", "package.json")).json()).toEqual({ name: "pkg1", }); }); test("adding workspace in workspace edits package.json with correct version (workspace:*)", async () => { await Promise.all([ write( join(packageDir, "package.json"), JSON.stringify({ name: "foo", workspaces: ["packages/*", "apps/*"], }), ), write( join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "pkg1", version: "1.0.0", }), ), write( join(packageDir, "apps", "pkg2", "package.json"), JSON.stringify({ name: "pkg2", version: "1.0.0", }), ), ]); const { stdout, exited } = Bun.spawn({ cmd: [bunExe(), "add", "pkg2@workspace:*"], cwd: join(packageDir, "packages", "pkg1"), stdout: "pipe", stderr: "inherit", env, }); const out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun add v1."), "", "installed pkg2@workspace:apps/pkg2", "", "2 packages installed", ]); expect(await exited).toBe(0); expect(await Bun.file(join(packageDir, "packages", "pkg1", "package.json")).json()).toEqual({ name: "pkg1", version: "1.0.0", dependencies: { pkg2: "workspace:*", }, }); }); test("workspaces with invalid versions should still install", async () => { await Promise.all([ write( join(packageDir, "package.json"), JSON.stringify({ name: "foo", version: "📦", workspaces: ["packages/*"], dependencies: { emoji1: "workspace:*", emoji2: "workspace:>=0", pre: "*", build: "workspace:^", }, }), ), write( join(packageDir, "packages", "emoji1", "package.json"), JSON.stringify({ name: "emoji1", version: "😃", }), ), write( join(packageDir, "packages", "emoji2", "package.json"), JSON.stringify({ name: "emoji2", version: "👀", }), ), write( join(packageDir, "packages", "pre", "package.json"), JSON.stringify({ name: "pre", version: "3.0.0_pre", }), ), write( join(packageDir, "packages", "build", "package.json"), JSON.stringify({ name: "build", version: "3.0.0_pre+bui_ld", }), ), ]); await runBunInstall(env, packageDir); const results = await Promise.all([ file(join(packageDir, "node_modules", "emoji1", "package.json")).json(), file(join(packageDir, "node_modules", "emoji2", "package.json")).json(), file(join(packageDir, "node_modules", "pre", "package.json")).json(), file(join(packageDir, "node_modules", "build", "package.json")).json(), ]); expect(results[0]).toEqual({ name: "emoji1", version: "😃", }); expect(results[1]).toEqual({ name: "emoji2", version: "👀", }); expect(results[2]).toEqual({ name: "pre", version: "3.0.0_pre", }); expect(results[3]).toEqual({ name: "build", version: "3.0.0_pre+bui_ld", }); }); describe("workspace aliases", async () => { test("combination", async () => { await Promise.all([ write( join(packageDir, "package.json"), JSON.stringify({ name: "foo", workspaces: ["packages/*"], dependencies: { "a0": "workspace:@org/a@latest", }, }), ), write( join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "@org/a", dependencies: { "a1": "workspace:@org/b@ ", "a2": "workspace:c@*", }, }), ), write( join(packageDir, "packages", "pkg2", "package.json"), JSON.stringify({ name: "@org/b", dependencies: { "a3": "workspace:c@ ", "a4": "workspace:@org/a@latest", }, }), ), write( join(packageDir, "packages", "pkg3", "package.json"), JSON.stringify({ name: "c", dependencies: { "a5": "workspace:@org/a@*", }, }), ), ]); console.log({ packageDir }); await runBunInstall(env, packageDir); const files = await Promise.all( ["a0", "a1", "a2", "a3", "a4", "a5"].map(name => file(join(packageDir, "node_modules", name, "package.json")).json(), ), ); expect(files).toMatchObject([ { name: "@org/a" }, { name: "@org/b" }, { name: "c" }, { name: "c" }, { name: "@org/a" }, { name: "@org/a" }, ]); }); var shouldPass: string[] = [ "workspace:@org/b@latest", "workspace:@org/b@*", // missing version after `@` "workspace:@org/b@", ]; for (const version of shouldPass) { test(`version range ${version} and workspace with no version`, async () => { await Promise.all([ write( join(packageDir, "package.json"), JSON.stringify({ name: "foo", workspaces: ["packages/*"], }), ), write( join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "@org/a", dependencies: { "a1": version, }, }), ), write( join(packageDir, "packages", "pkg2", "package.json"), JSON.stringify({ name: "@org/b", }), ), ]); await runBunInstall(env, packageDir); const files = await Promise.all([ file(join(packageDir, "node_modules", "@org", "a", "package.json")).json(), file(join(packageDir, "node_modules", "@org", "b", "package.json")).json(), file(join(packageDir, "node_modules", "a1", "package.json")).json(), ]); expect(files).toMatchObject([{ name: "@org/a" }, { name: "@org/b" }, { name: "@org/b" }]); }); } let shouldFail: string[] = ["workspace:@org/b@1.0.0", "workspace:@org/b@1", "workspace:@org/b"]; for (const version of shouldFail) { test(`version range ${version} and workspace with no version (should fail)`, async () => { await Promise.all([ write( join(packageDir, "package.json"), JSON.stringify({ name: "foo", workspaces: ["packages/*"], }), ), write( join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "@org/a", dependencies: { "a1": version, }, }), ), write( join(packageDir, "packages", "pkg2", "package.json"), JSON.stringify({ name: "@org/b", }), ), ]); const { stderr, exited } = Bun.spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "ignore", stderr: "pipe", env, }); const err = await Bun.readableStreamToText(stderr); if (version === "workspace:@org/b") { expect(err).toContain('Workspace dependency "a1" not found'); } else { expect(err).toContain(`No matching version for workspace dependency "a1". Version: "${version}"`); } expect(await exited).toBe(1); }); } }); for (const glob of [true, false]) { test(`does not crash when root package.json is in "workspaces"${glob ? " (glob)" : ""}`, async () => { await Promise.all([ write( join(packageDir, "package.json"), JSON.stringify({ name: "foo", workspaces: glob ? ["**"] : ["pkg1", "./*"], }), ), write( join(packageDir, "pkg1", "package.json"), JSON.stringify({ name: "pkg1", }), ), ]); await runBunInstall(env, packageDir); expect(await file(join(packageDir, "node_modules", "pkg1", "package.json")).json()).toEqual({ name: "pkg1", }); }); } test("cwd in workspace script is not the symlink path on windows", async () => { await Promise.all([ write( join(packageDir, "package.json"), JSON.stringify({ name: "foo", workspaces: ["pkg1"], }), ), write( join(packageDir, "pkg1", "package.json"), JSON.stringify({ name: "pkg1", scripts: { postinstall: 'bun -e \'require("fs").writeFileSync("cwd", process.cwd())\'', }, }), ), ]); await runBunInstall(env, packageDir); expect(await file(join(packageDir, "node_modules", "pkg1", "cwd")).text()).toBe(join(packageDir, "pkg1")); }); describe("relative tarballs", async () => { test("from package.json", async () => { await Promise.all([ write( join(packageDir, "package.json"), JSON.stringify({ name: "foo", workspaces: ["pkgs/*"], }), ), write( join(packageDir, "pkgs", "pkg1", "package.json"), JSON.stringify({ name: "pkg1", dependencies: { "qux": "../../qux-0.0.2.tgz", }, }), ), cp(join(import.meta.dir, "qux-0.0.2.tgz"), join(packageDir, "qux-0.0.2.tgz")), ]); await runBunInstall(env, packageDir); expect(await file(join(packageDir, "node_modules", "qux", "package.json")).json()).toMatchObject({ name: "qux", version: "0.0.2", }); }); test("from cli", async () => { await Promise.all([ write( join(packageDir, "package.json"), JSON.stringify({ name: "foo", workspaces: ["pkgs/*"], }), ), write( join(packageDir, "pkgs", "pkg1", "package.json"), JSON.stringify({ name: "pkg1", }), ), cp(join(import.meta.dir, "qux-0.0.2.tgz"), join(packageDir, "qux-0.0.2.tgz")), ]); const { stderr, exited } = Bun.spawn({ cmd: [bunExe(), "install", "../../qux-0.0.2.tgz"], cwd: join(packageDir, "pkgs", "pkg1"), stdout: "ignore", stderr: "pipe", env, }); const err = await Bun.readableStreamToText(stderr); expect(err).not.toContain("error:"); expect(err).not.toContain("failed to resolve"); expect(await exited).toBe(0); const results = await Promise.all([ file(join(packageDir, "node_modules", "qux", "package.json")).json(), file(join(packageDir, "pkgs", "pkg1", "package.json")).json(), ]); expect(results[0]).toMatchObject({ name: "qux", version: "0.0.2", }); expect(results[1]).toMatchObject({ name: "pkg1", dependencies: { qux: "../../qux-0.0.2.tgz", }, }); }); });