mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
614 lines
16 KiB
TypeScript
614 lines
16 KiB
TypeScript
import { spawnSync } from "bun";
|
|
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
|
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
|
import { existsSync, mkdtempSync, realpathSync } from "node:fs";
|
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
describe.each(["why", "pm why"])("bun %s", cmd => {
|
|
let package_dir: string;
|
|
let i = 0;
|
|
|
|
beforeAll(async () => {
|
|
const base = mkdtempSync(join(realpathSync(tmpdir()), "why-test-"));
|
|
|
|
package_dir = join(base, `why-test-${Math.random().toString(36).slice(2)}`);
|
|
await mkdir(package_dir, { recursive: true });
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (existsSync(package_dir)) {
|
|
await rm(package_dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
function setupTestWithDependencies() {
|
|
const testDir = tempDirWithFiles(`why-${i++}`, {
|
|
"package.json": JSON.stringify(
|
|
{
|
|
name: "test-package",
|
|
version: "1.0.0",
|
|
dependencies: {
|
|
"lodash": "^4.17.21",
|
|
"react": "^18.0.0",
|
|
},
|
|
devDependencies: {
|
|
"@types/react": "^18.0.0",
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
});
|
|
|
|
spawnSync({
|
|
cmd: [bunExe(), "install", "--lockfile-only"],
|
|
cwd: testDir,
|
|
env: bunEnv,
|
|
});
|
|
|
|
return testDir;
|
|
}
|
|
|
|
function setupComplexDependencyTree() {
|
|
const testDir = tempDirWithFiles(`why-complex-${i++}`, {
|
|
"package.json": JSON.stringify(
|
|
{
|
|
name: "complex-package",
|
|
version: "1.0.0",
|
|
dependencies: {
|
|
"express": "^4.18.2",
|
|
"react": "^18.0.0",
|
|
"react-dom": "^18.0.0",
|
|
},
|
|
devDependencies: {
|
|
"@types/express": "^4.17.17",
|
|
"typescript": "^5.0.0",
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
});
|
|
|
|
spawnSync({
|
|
cmd: [bunExe(), "install", "--lockfile-only"],
|
|
cwd: testDir,
|
|
env: bunEnv,
|
|
});
|
|
|
|
return testDir;
|
|
}
|
|
|
|
it("should show help when no package is specified", async () => {
|
|
const testDir = setupTestWithDependencies();
|
|
|
|
const { stdout, exitCode } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" ")],
|
|
cwd: testDir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
expect(stdout.toString()).toContain(`bun why v${Bun.version.replace("-debug", "")}`);
|
|
expect(exitCode).toBe(1);
|
|
});
|
|
|
|
it("should show direct dependency", async () => {
|
|
await writeFile(
|
|
join(package_dir, "package.json"),
|
|
JSON.stringify({
|
|
name: "foo",
|
|
version: "0.0.1",
|
|
dependencies: {
|
|
lodash: "^4.17.21",
|
|
},
|
|
}),
|
|
);
|
|
|
|
const install = spawnSync({
|
|
cmd: [bunExe(), "install", "--lockfile-only"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
expect(install.exitCode).toBe(0);
|
|
|
|
const { stdout, exitCode } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" "), "lodash"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
expect(exitCode).toBe(0);
|
|
const output = stdout.toString();
|
|
|
|
expect(output).toContain("lodash@");
|
|
expect(output).toContain("foo");
|
|
expect(output).toContain("requires ^4.17.21");
|
|
});
|
|
|
|
it("should show nested dependencies", async () => {
|
|
await writeFile(
|
|
join(package_dir, "package.json"),
|
|
JSON.stringify({
|
|
name: "foo",
|
|
version: "0.0.1",
|
|
dependencies: {
|
|
express: "^4.18.2",
|
|
},
|
|
}),
|
|
);
|
|
|
|
const install = spawnSync({
|
|
cmd: [bunExe(), "install", "--lockfile-only"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
expect(install.exitCode).toBe(0);
|
|
|
|
const { stdout, exitCode } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" "), "mime-types"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
expect(exitCode).toBe(0);
|
|
const output = stdout.toString();
|
|
expect(output).toContain("mime-types@");
|
|
|
|
expect(output).toContain("accepts@");
|
|
expect(output).toContain("express@");
|
|
});
|
|
|
|
it("should handle workspace dependencies", async () => {
|
|
await writeFile(
|
|
join(package_dir, "package.json"),
|
|
JSON.stringify({
|
|
name: "workspace-root",
|
|
version: "1.0.0",
|
|
workspaces: ["packages/*"],
|
|
}),
|
|
);
|
|
|
|
await mkdir(join(package_dir, "packages", "pkg-a"), { recursive: true });
|
|
await mkdir(join(package_dir, "packages", "pkg-b"), { recursive: true });
|
|
|
|
await writeFile(
|
|
join(package_dir, "packages", "pkg-a", "package.json"),
|
|
JSON.stringify({
|
|
name: "pkg-a",
|
|
version: "1.0.0",
|
|
dependencies: {
|
|
lodash: "^4.17.21",
|
|
},
|
|
}),
|
|
);
|
|
|
|
await writeFile(
|
|
join(package_dir, "packages", "pkg-b", "package.json"),
|
|
JSON.stringify({
|
|
name: "pkg-b",
|
|
version: "1.0.0",
|
|
dependencies: {
|
|
"pkg-a": "workspace:*",
|
|
},
|
|
}),
|
|
);
|
|
|
|
const install = spawnSync({
|
|
cmd: [bunExe(), "install", "--lockfile-only"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
expect(install.exitCode).toBe(0);
|
|
|
|
const { stdout, exitCode } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" "), "pkg-a"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
expect(exitCode).toBe(0);
|
|
const output = stdout.toString();
|
|
expect(output).toContain("pkg-a@");
|
|
expect(output).toContain("pkg-b@");
|
|
});
|
|
|
|
it("should handle npm aliases", async () => {
|
|
await writeFile(
|
|
join(package_dir, "package.json"),
|
|
JSON.stringify({
|
|
name: "foo",
|
|
version: "0.0.1",
|
|
dependencies: {
|
|
"alias-pkg": "npm:lodash@^4.17.21",
|
|
},
|
|
}),
|
|
);
|
|
|
|
const install = spawnSync({
|
|
cmd: [bunExe(), "install", "--lockfile-only"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
expect(install.exitCode).toBe(0);
|
|
|
|
const { stdout, stderr, exitCode } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" "), "alias-pkg"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
if (exitCode === 0) {
|
|
const output = stdout.toString();
|
|
expect(output).toContain("alias-pkg@");
|
|
} else {
|
|
expect(true).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("should show error for non-existent package", async () => {
|
|
await writeFile(
|
|
join(package_dir, "package.json"),
|
|
JSON.stringify({
|
|
name: "foo",
|
|
version: "0.0.1",
|
|
dependencies: {
|
|
lodash: "^4.17.21",
|
|
},
|
|
}),
|
|
);
|
|
|
|
const install = spawnSync({
|
|
cmd: [bunExe(), "install", "--lockfile-only"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
expect(install.exitCode).toBe(0);
|
|
|
|
const { stdout, stderr, exitCode } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" "), "non-existent-package"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
expect(exitCode).toBe(1);
|
|
|
|
const combinedOutput = stdout.toString() + stderr.toString();
|
|
|
|
expect(combinedOutput.includes("No packages matching") || combinedOutput.includes("not found in lockfile")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("should show dependency types correctly", async () => {
|
|
await writeFile(
|
|
join(package_dir, "package.json"),
|
|
JSON.stringify({
|
|
name: "foo",
|
|
version: "0.0.1",
|
|
dependencies: {
|
|
"express": "^4.18.2",
|
|
},
|
|
devDependencies: {
|
|
"typescript": "^5.0.0",
|
|
},
|
|
peerDependencies: {
|
|
"react": "^18.0.0",
|
|
},
|
|
optionalDependencies: {
|
|
"chalk": "^5.0.0",
|
|
},
|
|
}),
|
|
);
|
|
|
|
const install = spawnSync({
|
|
cmd: [bunExe(), "install", "--lockfile-only"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
expect(install.exitCode).toBe(0);
|
|
|
|
const { stdout: devStdout, exitCode: devExited } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" "), "typescript"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
expect(devExited).toBe(0);
|
|
const devOutput = devStdout.toString();
|
|
expect(devOutput).toContain("dev");
|
|
|
|
const { stdout: peerStdout, exitCode: peerExited } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" "), "react"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
expect(peerExited).toBe(0);
|
|
const peerOutput = peerStdout.toString();
|
|
expect(peerOutput).toContain("peer");
|
|
|
|
const { stdout: optStdout, exitCode: optExited } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" "), "chalk"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
expect(optExited).toBe(0);
|
|
const optOutput = optStdout.toString();
|
|
expect(optOutput).toContain("optional");
|
|
});
|
|
|
|
it("should handle packages with multiple versions", async () => {
|
|
await writeFile(
|
|
join(package_dir, "package.json"),
|
|
JSON.stringify({
|
|
name: "multi-version-test",
|
|
version: "1.0.0",
|
|
dependencies: {
|
|
"react": "^18.0.0",
|
|
"old-package": "npm:react@^16.0.0",
|
|
},
|
|
}),
|
|
);
|
|
|
|
const install = spawnSync({
|
|
cmd: [bunExe(), "install", "--lockfile-only"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
expect(install.exitCode).toBe(0);
|
|
|
|
const { stdout, exitCode } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" "), "react"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
expect(exitCode).toBe(0);
|
|
const output = stdout.toString();
|
|
|
|
expect(output).toContain("react@");
|
|
});
|
|
|
|
it("should handle deeply nested dependencies", async () => {
|
|
const testDir = setupComplexDependencyTree();
|
|
|
|
const { stdout, exitCode } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" "), "mime-db"],
|
|
cwd: testDir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
expect(exitCode).toBe(0);
|
|
const output = stdout.toString();
|
|
|
|
expect(output).toContain("mime-db@");
|
|
expect(output).toContain("mime-types@");
|
|
|
|
const lines = output.split("\n");
|
|
const indentedLines = lines.filter(line => line.includes(" "));
|
|
expect(indentedLines.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("should support glob patterns for package names", async () => {
|
|
const testDir = setupComplexDependencyTree();
|
|
|
|
const { stdout, exitCode } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" "), "@types/*"],
|
|
cwd: testDir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
expect(exitCode).toBe(0);
|
|
const output = stdout.toString();
|
|
expect(output).toContain("@types/");
|
|
expect(output).toContain("dev");
|
|
});
|
|
|
|
it("should support version constraints in the query", async () => {
|
|
await writeFile(
|
|
join(package_dir, "package.json"),
|
|
JSON.stringify({
|
|
name: "version-test",
|
|
version: "1.0.0",
|
|
dependencies: {
|
|
"react": "^18.0.0",
|
|
"lodash": "^4.17.21",
|
|
},
|
|
}),
|
|
);
|
|
|
|
const install = spawnSync({
|
|
cmd: [bunExe(), "install", "--lockfile-only"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
expect(install.exitCode).toBe(0);
|
|
|
|
const { stdout, exitCode } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" "), "react@^18.0.0"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
if (exitCode === 0) {
|
|
const output = stdout.toString();
|
|
expect(output).toContain("react@");
|
|
} else {
|
|
expect(true).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("should handle nested workspaces", async () => {
|
|
await writeFile(
|
|
join(package_dir, "package.json"),
|
|
JSON.stringify({
|
|
name: "workspace-root",
|
|
version: "1.0.0",
|
|
workspaces: ["packages/*", "apps/*"],
|
|
}),
|
|
);
|
|
|
|
await mkdir(join(package_dir, "packages", "pkg-a"), { recursive: true });
|
|
await mkdir(join(package_dir, "packages", "pkg-b"), { recursive: true });
|
|
await mkdir(join(package_dir, "apps", "app-a"), { recursive: true });
|
|
|
|
await writeFile(
|
|
join(package_dir, "packages", "pkg-a", "package.json"),
|
|
JSON.stringify({
|
|
name: "pkg-a",
|
|
version: "1.0.0",
|
|
dependencies: {
|
|
lodash: "^4.17.21",
|
|
},
|
|
}),
|
|
);
|
|
|
|
await writeFile(
|
|
join(package_dir, "packages", "pkg-b", "package.json"),
|
|
JSON.stringify({
|
|
name: "pkg-b",
|
|
version: "1.0.0",
|
|
dependencies: {
|
|
"pkg-a": "workspace:*",
|
|
},
|
|
}),
|
|
);
|
|
|
|
await writeFile(
|
|
join(package_dir, "apps", "app-a", "package.json"),
|
|
JSON.stringify({
|
|
name: "app-a",
|
|
version: "1.0.0",
|
|
dependencies: {
|
|
"pkg-b": "workspace:*",
|
|
},
|
|
}),
|
|
);
|
|
|
|
const install = spawnSync({
|
|
cmd: [bunExe(), "install", "--lockfile-only"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
expect(install.exitCode).toBe(0);
|
|
|
|
const { stdout, exitCode } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" "), "lodash"],
|
|
cwd: package_dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
expect(exitCode).toBe(0);
|
|
const output = stdout.toString();
|
|
expect(output).toContain("lodash@");
|
|
expect(output).toContain("pkg-a");
|
|
|
|
const lines = output.split("\n");
|
|
expect(lines.some(line => line.includes("pkg-a"))).toBe(true);
|
|
});
|
|
|
|
it("should support the --top flag to limit dependency tree depth", async () => {
|
|
const testDir = setupComplexDependencyTree();
|
|
|
|
const { stdout: stdoutWithTop, exitCode: exitedWithTop } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" "), "mime-db", "--top"],
|
|
cwd: testDir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
expect(exitedWithTop).toBe(0);
|
|
const outputWithTop = stdoutWithTop.toString();
|
|
|
|
const { stdout: stdoutWithoutTop, exitCode: exitedWithoutTop } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" "), "mime-db"],
|
|
cwd: testDir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
expect(exitedWithoutTop).toBe(0);
|
|
const outputWithoutTop = stdoutWithoutTop.toString();
|
|
|
|
expect(outputWithTop.length).toBeGreaterThan(0);
|
|
expect(outputWithoutTop.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("should support the --depth flag to limit dependency tree depth", async () => {
|
|
const testDir = setupComplexDependencyTree();
|
|
|
|
const { stdout: stdoutDepth2, exitCode: exitedDepth2 } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" "), "mime-db", "--depth", "2"],
|
|
cwd: testDir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
expect(exitedDepth2).toBe(0);
|
|
const outputDepth2 = stdoutDepth2.toString();
|
|
|
|
const { stdout: stdoutNoDepth, exitCode: exitedNoDepth } = spawnSync({
|
|
cmd: [bunExe(), ...cmd.split(" "), "mime-db"],
|
|
cwd: testDir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
expect(exitedNoDepth).toBe(0);
|
|
const outputNoDepth = stdoutNoDepth.toString();
|
|
|
|
expect(outputDepth2.split("\n").length).toBeLessThan(outputNoDepth.split("\n").length);
|
|
|
|
expect(outputDepth2).toContain("mime-db@");
|
|
});
|
|
});
|