Files
bun-src/test/cli/install/bun-pm-licenses.test.ts
2026-08-27 21:09:14 +00:00

1750 lines
72 KiB
TypeScript

import { spawn } from "bun";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "fs";
import { VerdaccioRegistry, bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness";
import { isAbsolute, join, sep } from "path";
import { pathToFileURL } from "url";
type Linker = "hoisted" | "isolated";
type Files = Record<string, string>;
type LicenseEntry = {
name: string;
versions: string[];
paths?: string[];
license: string;
homepage?: string;
author?: string;
description?: string;
};
const registry = new VerdaccioRegistry();
beforeAll(async () => {
await registry.start();
});
afterAll(() => {
registry.stop();
});
// CI exports BUN_INSTALL_CACHE_DIR, which overrides the harness bunfig's per-test `cache`; concurrent cases sharing one cache race on Windows and share hardlinked manifests on Linux.
const installEnv = (dir: string) => ({ ...bunEnv, BUN_INSTALL_CACHE_DIR: join(dir, ".bun-cache") });
const gitEnv = {
...bunEnv,
GIT_CONFIG_NOSYSTEM: "1",
GIT_AUTHOR_NAME: "Test",
GIT_AUTHOR_EMAIL: "[email protected]",
GIT_COMMITTER_NAME: "Test",
GIT_COMMITTER_EMAIL: "[email protected]",
};
async function gitRepo(manifest: Record<string, unknown>) {
const repoDir = tempDir("licenses-git-repo", { "package.json": JSON.stringify(manifest) });
for (const args of [
["init", "-q"],
["add", "package.json"],
["commit", "-q", "-m", "init", "--no-gpg-sign"],
]) {
await using proc = spawn({
cmd: ["git", ...args],
cwd: String(repoDir),
env: gitEnv,
stdout: "ignore",
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(stderr).not.toContain("fatal:");
expect(exitCode).toBe(0);
}
return repoDir;
}
const emptyText = (checked: number) => `No packages to list (checked ${checked} packages in bun.lock)`;
function expectEmptyText(stdout: string, checked: number) {
expect(normalizeBunSnapshot(stdout)).toBe(emptyText(checked));
expect(stdout).toMatch(/^No packages to list \(checked \d+ packages in bun\.lock\) \[\d+\.\d+m?s\]\n$/);
}
const MISSING_NOTE = "note: run 'bun install' first";
const fixturePackageJson = JSON.stringify({
name: "licenses-fixture",
version: "1.0.0",
dependencies: {
"resolve": "1.9.0",
"no-deps": "1.0.0",
"one-dep": "1.0.0",
},
devDependencies: {
"a-dep": "1.0.1",
},
});
function pkg(fields: Record<string, unknown>) {
return JSON.stringify({ name: "licenses-fixture", version: "1.0.0", ...fields });
}
// Shape of pnpm's commands/test/licenses/fixtures/workspace-licenses: dependency-less private root, foo with deps+devDeps, bar with deps.
const monorepoFiles: Files = {
"package.json": JSON.stringify({ name: "mono", private: true, workspaces: ["packages/*"] }),
"packages/foo/package.json": JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: { "no-deps": "1.0.0" },
devDependencies: { "a-dep": "1.0.1" },
}),
"packages/bar/package.json": JSON.stringify({
name: "bar",
version: "1.0.0",
dependencies: { resolve: "1.9.0" },
}),
};
async function install(dir: string, linker: Linker, ...args: string[]) {
await using proc = spawn({
cmd: [bunExe(), "install", "--linker", linker, ...args],
env: installEnv(dir),
cwd: dir,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
}
async function setup(linker: Linker = "hoisted", files: Files = { "package.json": fixturePackageJson }) {
const { packageDir } = await registry.createTestDir({ bunfigOpts: { linker }, files });
await install(packageDir, linker);
return packageDir;
}
// `bun install --production` never writes a lockfile, so the lockfile (with its dev-inclusive tree) is created first.
async function setupProductionInstall(files: Files) {
const { packageDir } = await registry.createTestDir({ bunfigOpts: { linker: "hoisted" }, files });
await install(packageDir, "hoisted", "--lockfile-only");
await install(packageDir, "hoisted", "--production");
return packageDir;
}
async function licenses(dir: string, ...args: string[]): Promise<[string, string, number]> {
await using proc = spawn({
cmd: [bunExe(), "pm", "licenses", ...args],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
return await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
}
async function licensesText(dir: string, ...args: string[]) {
const [stdout, stderr, exitCode] = await licenses(dir, ...args);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
return stdout;
}
function stripPaths(parsed: Record<string, LicenseEntry[]>) {
for (const entries of Object.values(parsed)) {
for (const entry of entries) {
expect(entry.paths).toBeArrayOfSize(entry.versions.length);
for (const p of entry.paths!) {
expect(isAbsolute(p)).toBeTrue();
expect(existsSync(join(p, "package.json"))).toBeTrue();
}
delete entry.paths;
}
}
return parsed;
}
async function licensesJsonRaw(dir: string, ...args: string[]) {
const [stdout, stderr, exitCode] = await licenses(dir, ...args, "--json");
expect(stderr).toBe("");
const parsed = JSON.parse(stdout);
expect(exitCode).toBe(0);
return parsed as Record<string, LicenseEntry[]>;
}
async function licensesJson(dir: string, ...args: string[]) {
return stripPaths(await licensesJsonRaw(dir, ...args));
}
async function licensesEntries(dir: string, ...args: string[]): Promise<LicenseEntry[]> {
return Object.values(await licensesJsonRaw(dir, ...args)).flat();
}
function pathsOf(entries: LicenseEntry[], name: string) {
return entries.find(entry => entry.name === name)!.paths;
}
const nm = (dir: string, ...rest: string[]) => join(dir, "node_modules", ...rest);
const store = (dir: string, entry: string, ...name: string[]) =>
join(dir, "node_modules", ".bun", entry, "node_modules", ...name);
async function pm(dir: string, ...args: string[]): Promise<[string, string, number]> {
await using proc = spawn({
cmd: [bunExe(), "pm", ...args],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
return await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
}
function names(parsed: Record<string, { name: string }[]>) {
return Object.values(parsed)
.flat()
.map(entry => entry.name)
.sort();
}
const u = (name: string, ...versions: string[]): LicenseEntry => ({ name, versions, license: "Unknown" });
// On Linux/Windows installed files are hardlinks into the cache; unlink first so the cache copy is left alone.
function overwriteInstalledManifest(dir: string, pkg: string, contents: string) {
const path = join(dir, "node_modules", pkg, "package.json");
rmSync(path);
writeFileSync(path, contents);
}
function patchInstalledManifest(dir: string, pkg: string, fields: Record<string, unknown>) {
const path = join(dir, "node_modules", pkg, "package.json");
const manifest = { ...JSON.parse(readFileSync(path, "utf8")), ...fields };
for (const [key, value] of Object.entries(fields)) if (value === undefined) delete manifest[key];
overwriteInstalledManifest(dir, pkg, JSON.stringify(manifest));
}
const pathParseDescription = "Node.js path.parse() ponyfill";
const resolveDescription = "resolve like require.resolve() on behalf of files asynchronously and synchronously";
const resolveAuthor = "James Halliday <[email protected]> (http://substack.net)";
// [email protected]'s manifest has no `homepage`; the link falls back to its `repository.url`.
const resolveHomepage = "git://github.com/browserify/resolve.git";
const fullJson = {
MIT: [
{
name: "path-parse",
versions: ["1.0.6"],
license: "MIT",
author: "Javier Blanco <http://jbgutierrez.info>",
description: pathParseDescription,
homepage: "https://github.com/jbgutierrez/path-parse#readme",
},
{
name: "resolve",
versions: ["1.9.0"],
license: "MIT",
author: resolveAuthor,
description: resolveDescription,
homepage: resolveHomepage,
},
],
Unknown: [u("a-dep", "1.0.1"), u("no-deps", "1.0.0", "1.0.1"), u("one-dep", "1.0.0")],
};
const prodJson = {
MIT: fullJson.MIT,
Unknown: [u("no-deps", "1.0.0", "1.0.1"), u("one-dep", "1.0.0")],
};
const monoJson = { MIT: fullJson.MIT, Unknown: [u("a-dep", "1.0.1"), u("no-deps", "1.0.0")] };
const fooJson = { Unknown: [u("a-dep", "1.0.1"), u("no-deps", "1.0.0")] };
const barJson = { MIT: fullJson.MIT };
const monoNames = ["a-dep", "no-deps", "path-parse", "resolve"];
describe("bun pm licenses", () => {
let hoistedDir: string;
let monoDir: string;
beforeAll(async () => {
[hoistedDir, monoDir] = await Promise.all([setup(), setup("hoisted", monorepoFiles)]);
});
test.concurrent("text output groups packages by license, Unknown last, dev-only packages marked", async () => {
const [stdout, stderr, exitCode] = await licenses(hoistedDir);
expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(`
"MIT (2)
├── [email protected]
└── [email protected]
Unknown (4)
├── [email protected] (dev)
├── [email protected]
├── [email protected]
└── [email protected]"
`);
expect(stdout.split("\n").filter(line => line.endsWith(" (dev)"))).toStrictEqual(["├── [email protected] (dev)"]);
expect(stdout).not.toContain(hoistedDir);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
test.concurrent("--json shape", async () => {
const [stdout, stderr, exitCode] = await licenses(hoistedDir, "--json");
expect(stderr).toBe("");
const parsed = JSON.parse(stdout);
expect(parsed).toStrictEqual({
MIT: [
{ ...fullJson.MIT[0], paths: [nm(hoistedDir, "path-parse")] },
{ ...fullJson.MIT[1], paths: [nm(hoistedDir, "resolve")] },
],
Unknown: [
{ ...u("a-dep", "1.0.1"), paths: [nm(hoistedDir, "a-dep")] },
{
...u("no-deps", "1.0.0", "1.0.1"),
paths: [nm(hoistedDir, "no-deps"), nm(hoistedDir, "one-dep", "node_modules", "no-deps")],
},
{ ...u("one-dep", "1.0.0"), paths: [nm(hoistedDir, "one-dep")] },
],
});
// Same field order as the lines --long prints under an entry.
const keyOrder = ["name", "versions", "paths", "license", "author", "description", "homepage"];
expect(Object.keys(parsed.MIT[0])).toStrictEqual(keyOrder);
expect(Object.keys(parsed.MIT[1])).toStrictEqual(keyOrder);
expect(Object.keys(parsed.Unknown[0])).toStrictEqual(["name", "versions", "paths", "license"]);
expect(stripPaths(structuredClone(parsed))).toStrictEqual(fullJson);
expect(Object.keys(parsed)).toStrictEqual(["MIT", "Unknown"]);
expect(parsed.MIT[0].description).toBe(pathParseDescription);
expect(parsed.MIT[1].homepage).toBe(resolveHomepage);
expect(parsed.Unknown[0]).not.toHaveProperty("description");
expect(parsed.Unknown[0]).not.toHaveProperty("homepage");
for (const [key, entries] of Object.entries(parsed as Record<string, LicenseEntry[]>)) {
expect(entries.map(entry => entry.license)).toStrictEqual(entries.map(() => key));
}
expect(stdout).not.toContain("(dev)");
expect(stdout).not.toContain('"dev"');
expect(exitCode).toBe(0);
});
test.concurrent("legacy license shapes", async () => {
const dir = await setup();
patchInstalledManifest(dir, "a-dep", { license: { type: "BSD-3-Clause", url: "x" } });
patchInstalledManifest(dir, "one-dep", { licenses: [{ type: "MIT" }, { type: "Apache-2.0" }] });
patchInstalledManifest(dir, "no-deps", { licenses: { type: "ISC" } });
const parsed = await licensesJson(dir);
expect(Object.keys(parsed)).toStrictEqual(["BSD-3-Clause", "ISC", "MIT", "(MIT OR Apache-2.0)", "Unknown"]);
expect(parsed["ISC"]).toStrictEqual([{ name: "no-deps", versions: ["1.0.0"], license: "ISC" }]);
expect(parsed["Unknown"]).toStrictEqual([u("no-deps", "1.0.1")]);
expect(parsed["(MIT OR Apache-2.0)"]).toStrictEqual([
{ name: "one-dep", versions: ["1.0.0"], license: "(MIT OR Apache-2.0)" },
]);
expect(parsed["BSD-3-Clause"]).toStrictEqual([{ name: "a-dep", versions: ["1.0.1"], license: "BSD-3-Clause" }]);
expect(parsed["MIT"].map(entry => entry.name)).toStrictEqual(["path-parse", "resolve"]);
});
test.concurrent("groups are ordered case-insensitively, ignoring a leading parenthesis, Unknown last", async () => {
const dir = await setup("hoisted", {
"package.json": pkg({
dependencies: { "resolve": "1.9.0", "no-deps": "1.0.0", "one-dep": "1.0.0", "uses-a-dep-9": "1.0.0" },
devDependencies: { "a-dep": "1.0.1" },
}),
});
patchInstalledManifest(dir, "a-dep", { license: "mit" });
patchInstalledManifest(dir, "no-deps", { license: "ISC" });
patchInstalledManifest(dir, "one-dep/node_modules/no-deps", { license: "BSD-3-Clause" });
patchInstalledManifest(dir, "one-dep", { licenses: [{ type: "MIT" }, { type: "Apache-2.0" }] });
patchInstalledManifest(dir, "resolve", { license: "SEE LICENSE IN LICENSE.txt" });
patchInstalledManifest(dir, "uses-a-dep-9/node_modules/a-dep", { license: "zlib" });
const [stdout, stderr, exitCode] = await licenses(dir);
expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(`
"BSD-3-Clause (1)
└── [email protected]
ISC (1)
└── [email protected]
MIT (1)
└── [email protected]
mit (1)
└── [email protected] (dev)
(MIT OR Apache-2.0) (1)
└── [email protected]
SEE LICENSE IN LICENSE.txt (1)
└── [email protected]
zlib (1)
└── [email protected]
Unknown (1)
└── [email protected]"
`);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
const parsed = await licensesJson(dir);
expect(Object.keys(parsed)).toStrictEqual([
"BSD-3-Clause",
"ISC",
"MIT",
"mit",
"(MIT OR Apache-2.0)",
"SEE LICENSE IN LICENSE.txt",
"zlib",
"Unknown",
]);
expect(parsed.mit).toStrictEqual([{ name: "a-dep", versions: ["1.0.1"], license: "mit" }]);
expect(parsed.zlib).toStrictEqual([{ name: "a-dep", versions: ["1.0.9"], license: "zlib" }]);
expect(parsed.Unknown).toStrictEqual([u("uses-a-dep-9", "1.0.0")]);
});
// pnpm license-resolver/test/parseLicenseFromManifest.test.ts, replayed against installed manifests.
test.concurrent("`license` array and legacy `name` key", async () => {
const dir = await setup();
patchInstalledManifest(dir, "a-dep", { license: [{ type: "MIT" }, { type: "Apache-2.0" }] });
patchInstalledManifest(dir, "one-dep", { licenses: [{ name: "ISC" }] });
patchInstalledManifest(dir, "no-deps", { license: ["BSD-2-Clause"] });
patchInstalledManifest(dir, "one-dep/node_modules/no-deps", { license: { name: "0BSD" } });
const parsed = await licensesJson(dir);
expect(parsed["(MIT OR Apache-2.0)"]).toStrictEqual([
{ name: "a-dep", versions: ["1.0.1"], license: "(MIT OR Apache-2.0)" },
]);
expect(parsed["ISC"]).toStrictEqual([{ name: "one-dep", versions: ["1.0.0"], license: "ISC" }]);
expect(parsed["BSD-2-Clause"]).toStrictEqual([{ name: "no-deps", versions: ["1.0.0"], license: "BSD-2-Clause" }]);
expect(parsed["0BSD"]).toStrictEqual([{ name: "no-deps", versions: ["1.0.1"], license: "0BSD" }]);
expect(parsed).not.toHaveProperty("Unknown");
});
test.concurrent("empty `license` falls through to `licenses`; `license` wins when both are present", async () => {
const dir = await setup();
patchInstalledManifest(dir, "a-dep", { license: "", licenses: [{ type: "MIT" }] });
patchInstalledManifest(dir, "one-dep", { license: "Apache-2.0", licenses: [{ type: "MIT" }] });
const parsed = await licensesJson(dir);
expect(parsed["MIT"].map(entry => entry.name)).toStrictEqual(["a-dep", "path-parse", "resolve"]);
expect(parsed["Apache-2.0"]).toStrictEqual([{ name: "one-dep", versions: ["1.0.0"], license: "Apache-2.0" }]);
});
test.concurrent("non-string license shapes are Unknown; entries with non-string type are skipped", async () => {
const dir = await setup();
patchInstalledManifest(dir, "a-dep", { license: 42 });
patchInstalledManifest(dir, "one-dep", { licenses: [] });
patchInstalledManifest(dir, "no-deps", { licenses: [{ url: "x" }] });
patchInstalledManifest(dir, "resolve", { license: undefined, licenses: [{ type: 42 }, { type: "MIT" }] });
const parsed = await licensesJson(dir);
expect(parsed).toStrictEqual(fullJson);
});
test.concurrent("repeated legacy entries are not deduplicated", async () => {
const dir = await setup();
patchInstalledManifest(dir, "one-dep", { licenses: [{ type: "MIT" }, { type: "MIT" }, { type: "Apache-2.0" }] });
const parsed = await licensesJson(dir);
expect(parsed["(MIT OR MIT OR Apache-2.0)"]).toStrictEqual([
{ name: "one-dep", versions: ["1.0.0"], license: "(MIT OR MIT OR Apache-2.0)" },
]);
});
test.concurrent("one package with two versions: per-license grouping, metadata from the newest version", async () => {
const dir = await setup();
patchInstalledManifest(dir, "no-deps", { license: "MIT", homepage: "https://example.com/old" });
patchInstalledManifest(dir, "one-dep/node_modules/no-deps", {
license: "MIT",
homepage: "https://example.com/new",
});
const parsed = await licensesJson(dir);
expect(parsed["MIT"]).toStrictEqual([
{ name: "no-deps", versions: ["1.0.0", "1.0.1"], license: "MIT", homepage: "https://example.com/new" },
...fullJson.MIT,
]);
expect(parsed["Unknown"]).toStrictEqual([u("a-dep", "1.0.1"), u("one-dep", "1.0.0")]);
expect(pathsOf(await licensesEntries(dir), "no-deps")).toStrictEqual([
nm(dir, "no-deps"),
nm(dir, "one-dep", "node_modules", "no-deps"),
]);
});
test.concurrent("--json `license` follows each version's group; an empty description is omitted", async () => {
const dir = await setup();
patchInstalledManifest(dir, "no-deps", { license: "ISC", description: "" });
patchInstalledManifest(dir, "one-dep/node_modules/no-deps", { license: "0BSD", description: "newer" });
patchInstalledManifest(dir, "a-dep", { licenses: [{ type: "MIT" }, { type: "Apache-2.0" }] });
const parsed = await licensesJson(dir);
expect(parsed).toStrictEqual({
"(MIT OR Apache-2.0)": [{ name: "a-dep", versions: ["1.0.1"], license: "(MIT OR Apache-2.0)" }],
"0BSD": [{ name: "no-deps", versions: ["1.0.1"], license: "0BSD", description: "newer" }],
"ISC": [{ name: "no-deps", versions: ["1.0.0"], license: "ISC" }],
"MIT": fullJson.MIT,
"Unknown": [u("one-dep", "1.0.0")],
});
expect(Object.keys(parsed)).toStrictEqual(["0BSD", "ISC", "MIT", "(MIT OR Apache-2.0)", "Unknown"]);
});
test.concurrent("versions are ordered by semver, names bytewise", async () => {
const dir = await setup("hoisted", {
"package.json": pkg({ dependencies: { "uses-a-dep-9": "1.0.0", "uses-a-dep-10": "1.0.0" } }),
});
const [stdout, stderr, exitCode] = await licenses(dir);
expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(`
"Unknown (4)
├── [email protected]
├── [email protected]
├── [email protected]
└── [email protected]"
`);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
const parsed = await licensesJson(dir);
expect(parsed["Unknown"][0]).toStrictEqual(u("a-dep", "1.0.9", "1.0.10"));
});
test.concurrent("(dev) marks packages only reachable through devDependencies", async () => {
const dir = await setup("hoisted", {
"package.json": pkg({ dependencies: { "a-dep": "1.0.9" }, devDependencies: { "uses-a-dep-9": "1.0.0" } }),
});
const [stdout, stderr, exitCode] = await licenses(dir);
expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(`
"Unknown (2)
├── [email protected]
└── [email protected] (dev)"
`);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
expect(await licensesText(dir, "--prod")).not.toContain("(dev)");
const [json, jsonStderr, jsonExit] = await licenses(dir, "--json");
expect(jsonStderr).toBe("");
expect(json).not.toContain("(dev)");
expect(json).not.toContain('"dev"');
expect(stripPaths(JSON.parse(json))).toStrictEqual({ Unknown: [u("a-dep", "1.0.9"), u("uses-a-dep-9", "1.0.0")] });
expect(jsonExit).toBe(0);
expect(await licensesJson(dir, "--dev")).toStrictEqual({
Unknown: [u("a-dep", "1.0.9"), u("uses-a-dep-9", "1.0.0")],
});
expect(normalizeBunSnapshot(await licensesText(dir, "--dev"))).toMatchInlineSnapshot(`
"Unknown (2)
├── [email protected]
└── [email protected] (dev)"
`);
});
// pnpm detect-dep-types: the marker is per name@version and reaches the transitive dependencies of a devDependency.
test.concurrent("(dev) propagates through a devDependency's subtree; --dev lists that subtree", async () => {
const dir = await setup("hoisted", {
"package.json": pkg({
dependencies: { "no-deps": "1.0.1", "uses-a-dep-10": "1.0.0" },
devDependencies: { "one-dep": "1.0.0", "uses-a-dep-9": "1.0.0" },
}),
});
expect(normalizeBunSnapshot(await licensesText(dir))).toMatchInlineSnapshot(`
"Unknown (6)
├── [email protected] (dev)
├── [email protected]
├── [email protected]
├── [email protected] (dev)
├── [email protected]
└── [email protected] (dev)"
`);
expect(normalizeBunSnapshot(await licensesText(dir, "--dev"))).toMatchInlineSnapshot(`
"Unknown (4)
├── [email protected] (dev)
├── [email protected]
├── [email protected] (dev)
└── [email protected] (dev)"
`);
expect(await licensesJson(dir, "--dev")).toStrictEqual({
Unknown: [u("a-dep", "1.0.9"), u("no-deps", "1.0.1"), u("one-dep", "1.0.0"), u("uses-a-dep-9", "1.0.0")],
});
});
test.concurrent("--dev lists the devDependencies closure", async () => {
const dir = await setup("hoisted", {
"package.json": pkg({ dependencies: { "no-deps": "1.0.0" }, devDependencies: { "one-dep": "1.0.0" } }),
});
const expected = { Unknown: [u("no-deps", "1.0.1"), u("one-dep", "1.0.0")] };
expect(await licensesJson(dir, "--dev")).toStrictEqual(expected);
expect(await licensesJson(dir, "-D")).toStrictEqual(expected);
expect(normalizeBunSnapshot(await licensesText(dir, "--dev"))).toMatchInlineSnapshot(`
"Unknown (2)
├── [email protected] (dev)
└── [email protected] (dev)"
`);
expect(normalizeBunSnapshot(await licensesText(hoistedDir, "--dev"))).toMatchInlineSnapshot(`
"Unknown (1)
└── [email protected] (dev)"
`);
expect(await licensesJson(hoistedDir, "--dev")).toStrictEqual({ Unknown: [u("a-dep", "1.0.1")] });
});
test.concurrent("--dev in a workspace", async () => {
expect(await licensesJson(monoDir, "--dev")).toStrictEqual({ Unknown: [u("a-dep", "1.0.1")] });
expect(await licensesJson(join(monoDir, "packages", "foo"), "--dev")).toStrictEqual({
Unknown: [u("a-dep", "1.0.1")],
});
const bar = join(monoDir, "packages", "bar");
expectEmptyText(await licensesText(bar, "--dev"), 0);
expect(await licensesJson(bar, "--dev")).toStrictEqual({});
});
test.concurrent("--dev cannot be combined with --prod", async () => {
for (const args of [
["--dev", "--prod"],
["--dev", "--omit=dev"],
["--prod", "--dev", "--json"],
]) {
const [stdout, stderr, exitCode] = await licenses(hoistedDir, ...args);
expect(stdout).toBe("");
expect(normalizeBunSnapshot(stderr)).toBe("error: --dev cannot be combined with --prod or --omit=dev");
expect(exitCode).toBe(1);
}
});
test.concurrent.each(["--prod", "--production", "-p", "-P"])("%s omits devDependencies (--json)", async flag => {
expect(await licensesJson(hoistedDir, flag)).toStrictEqual(prodJson);
});
test.concurrent("--prod omits devDependencies (text)", async () => {
const [stdout, stderr, exitCode] = await licenses(hoistedDir, "--prod");
expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(`
"MIT (2)
├── [email protected]
└── [email protected]
Unknown (3)
├── [email protected]
├── [email protected]
└── [email protected]"
`);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
test.concurrent("--prod keeps optionalDependencies", async () => {
const dir = await setup("hoisted", {
"package.json": pkg({
dependencies: { "no-deps": "1.0.0" },
optionalDependencies: { "one-dep": "1.0.0" },
devDependencies: { "a-dep": "1.0.1" },
}),
});
expect(await licensesJson(dir, "--prod")).toStrictEqual({
Unknown: [u("no-deps", "1.0.0", "1.0.1"), u("one-dep", "1.0.0")],
});
expect(names(await licensesJson(dir))).toStrictEqual(["a-dep", "no-deps", "one-dep"]);
});
test.concurrent("os/cpu-skipped optional dependencies are omitted, their parent is listed", async () => {
const dir = await setup("hoisted", { "package.json": pkg({ dependencies: { "optional-native": "1.0.0" } }) });
const installedNatives = [
"native-bar-x64",
"native-foo-x64",
"native-foo-x86",
"native-libc-glibc",
"native-libc-musl",
]
.filter(name => existsSync(join(dir, "node_modules", name, "package.json")))
.map(name => u(name, "1.0.0"));
expect(installedNatives.map(entry => entry.name)).not.toContain("native-foo-x64");
expect(await licensesJson(dir)).toStrictEqual({
Unknown: [...installedNatives, u("optional-native", "1.0.0")],
});
});
test.concurrent("--long prints author, description and homepage under each entry", async () => {
const [stdout, stderr, exitCode] = await licenses(hoistedDir, "--long");
expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(`
"MIT (2)
├── [email protected]
│ Javier Blanco <http://jbgutierrez.info>
│ Node.js path.parse() ponyfill
│ https://github.com/jbgutierrez/path-parse#readme
└── [email protected]
James Halliday <[email protected]> (http://substack.net)
resolve like require.resolve() on behalf of files asynchronously and synchronously
git://github.com/browserify/resolve.git
Unknown (4)
├── [email protected] (dev)
├── [email protected]
├── [email protected]
└── [email protected]"
`);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
expect(await licensesText(hoistedDir, "ls", "--long")).toBe(stdout);
expect(stdout).not.toContain(nm(hoistedDir));
const [plainJson, longJson] = await Promise.all([
licensesText(hoistedDir, "--json"),
licensesText(hoistedDir, "--long", "--json"),
]);
expect(longJson).toBe(plainJson);
expect(stripPaths(JSON.parse(longJson))).toStrictEqual(fullJson);
});
test.concurrent("--long details are per version in text; --json takes them from the newest version", async () => {
const dir = await setup();
patchInstalledManifest(dir, "no-deps", { description: "only a description" });
patchInstalledManifest(dir, "a-dep", { author: { name: "Ann", email: "[email protected]" } });
const first = await licensesText(dir, "--long");
expect(first).toContain(
"├── [email protected] (dev)\n│ Ann <[email protected]>\n├── [email protected]\n│ only a description\n├── [email protected]\n└── [email protected]\n",
);
expect(await licensesJson(dir)).toStrictEqual({
MIT: fullJson.MIT,
Unknown: [
{ ...u("a-dep", "1.0.1"), author: "Ann <[email protected]>" },
u("no-deps", "1.0.0", "1.0.1"),
u("one-dep", "1.0.0"),
],
});
patchInstalledManifest(dir, "one-dep/node_modules/no-deps", {
description: "newest wins\nline two",
homepage: "https://example.com/new",
});
const second = await licensesText(dir, "--long");
expect(second).toContain(
"├── [email protected]\n│ only a description\n├── [email protected]\n│ newest winsline two\n│ https://example.com/new\n└── [email protected]\n",
);
expect(second.split("\n").some(line => line.startsWith("line two"))).toBeFalse();
const parsed = await licensesJson(dir);
expect(parsed.Unknown[1]).toStrictEqual({
...u("no-deps", "1.0.0", "1.0.1"),
homepage: "https://example.com/new",
description: "newest wins\nline two",
});
expect(JSON.stringify(parsed)).not.toContain("only a description");
});
test.concurrent("`repository` stands in for a missing `homepage`; `homepage` wins when both are set", async () => {
const dir = await setup();
patchInstalledManifest(dir, "a-dep", { repository: "https://github.com/example/a-dep.git" });
patchInstalledManifest(dir, "one-dep", {
repository: { type: "git", url: "git+ssh://[email protected]/example/one-dep.git" },
});
patchInstalledManifest(dir, "no-deps", {
homepage: "https://no-deps.example",
repository: { type: "git", url: "https://github.com/example/no-deps.git" },
});
patchInstalledManifest(dir, "one-dep/node_modules/no-deps", { repository: { type: "git" } });
patchInstalledManifest(dir, "resolve", { repository: undefined });
expect(await licensesJson(dir)).toStrictEqual({
MIT: [
fullJson.MIT[0],
{
name: "resolve",
versions: ["1.9.0"],
license: "MIT",
author: resolveAuthor,
description: resolveDescription,
},
],
Unknown: [
{ ...u("a-dep", "1.0.1"), homepage: "https://github.com/example/a-dep.git" },
u("no-deps", "1.0.0", "1.0.1"),
{ ...u("one-dep", "1.0.0"), homepage: "git+ssh://[email protected]/example/one-dep.git" },
],
});
expect(normalizeBunSnapshot(await licensesText(dir, "--long"))).toMatchInlineSnapshot(`
"MIT (2)
├── [email protected]
│ Javier Blanco <http://jbgutierrez.info>
│ Node.js path.parse() ponyfill
│ https://github.com/jbgutierrez/path-parse#readme
└── [email protected]
James Halliday <[email protected]> (http://substack.net)
resolve like require.resolve() on behalf of files asynchronously and synchronously
Unknown (4)
├── [email protected] (dev)
│ https://github.com/example/a-dep.git
├── [email protected]
│ https://no-deps.example
├── [email protected]
└── [email protected]
git+ssh://[email protected]/example/one-dep.git"
`);
});
test.concurrent(
"control characters from package.json are stripped in text output but preserved in --json",
async () => {
const dir = await setup();
const evilLicense = "MIT\u001b[31m\nEVIL";
patchInstalledManifest(dir, "a-dep", { license: evilLicense, description: "tab\there\r\n" });
patchInstalledManifest(dir, "one-dep", { license: "ISC\nGPL-3.0" });
patchInstalledManifest(dir, "no-deps", { license: "BSD\t2" });
const stdout = await licensesText(dir, "--long");
expect(stdout).not.toContain("\u001b");
expect(stdout).not.toContain("\r");
expect(stdout).not.toContain("\t");
expect(stdout).toContain("MIT[31mEVIL (1)\n└── [email protected] (dev)\n tabhere\n");
expect(stdout).toContain("ISCGPL-3.0 (1)\n└── [email protected]\n");
expect(stdout).toContain("BSD2 (1)\n└── [email protected]\n");
expect(stdout.split("\n").filter(line => / \(\d+\)$/.test(line))).toStrictEqual([
"BSD2 (1)",
"ISCGPL-3.0 (1)",
"MIT (2)",
"MIT[31mEVIL (1)",
"Unknown (1)",
]);
expect(stdout.split("\n").some(line => line.startsWith("GPL-3.0") || line.startsWith("EVIL"))).toBeFalse();
const parsed = await licensesJson(dir);
expect(Object.keys(parsed)).toStrictEqual(["BSD\t2", "ISC\nGPL-3.0", "MIT", evilLicense, "Unknown"]);
expect(parsed[evilLicense]).toStrictEqual([
{ name: "a-dep", versions: ["1.0.1"], license: evilLicense, description: "tab\there\r\n" },
]);
expect(parsed["ISC\nGPL-3.0"]).toStrictEqual([{ name: "one-dep", versions: ["1.0.0"], license: "ISC\nGPL-3.0" }]);
expect(parsed["BSD\t2"]).toStrictEqual([{ name: "no-deps", versions: ["1.0.0"], license: "BSD\t2" }]);
expect(parsed.Unknown).toStrictEqual([u("no-deps", "1.0.1")]);
},
);
test.concurrent("--long strips control characters from author, description and homepage", async () => {
const dir = await setup();
const author = "Eve\u001b]8;;https://evil.example\u0007click\u001b]8;;\u0007";
const description = "first\r\nsecond";
const homepage = "https://example.com/\u001b[2Jx";
patchInstalledManifest(dir, "a-dep", { author, description, homepage });
const stdout = await licensesText(dir, "--long");
expect(stdout).not.toContain("\u001b");
expect(stdout).not.toContain("\u0007");
expect(stdout).not.toContain("\r");
expect(stdout).toContain(
"├── [email protected] (dev)\n│ Eve]8;;https://evil.exampleclick]8;;\n│ firstsecond\n│ https://example.com/[2Jx\n├── [email protected]\n",
);
expect(stdout.split("\n").some(line => line.startsWith("second"))).toBeFalse();
expect((await licensesJson(dir)).Unknown[0]).toStrictEqual({
...u("a-dep", "1.0.1"),
author,
description,
homepage,
});
});
test.concurrent("isolated linker matches hoisted: marker, --dev and --long", async () => {
const dir = await setup("isolated");
const [[expected], [stdout, stderr, exitCode]] = await Promise.all([licenses(hoistedDir), licenses(dir)]);
expect(stdout).toBe(expected);
expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(`
"MIT (2)
├── [email protected]
└── [email protected]
Unknown (4)
├── [email protected] (dev)
├── [email protected]
├── [email protected]
└── [email protected]"
`);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
const [isoLong, hoistedLong, isoDev, hoistedDev, isoDevJson, hoistedDevJson, isoLongJson, hoistedLongJson] =
await Promise.all([
licensesText(dir, "--long"),
licensesText(hoistedDir, "--long"),
licensesText(dir, "--dev"),
licensesText(hoistedDir, "--dev"),
licensesJson(dir, "--dev"),
licensesJson(hoistedDir, "--dev"),
licensesJson(dir, "--long"),
licensesJson(hoistedDir, "--long"),
]);
expect(isoLong).toBe(hoistedLong);
expect(isoDev).toBe(hoistedDev);
expect(isoDevJson).toStrictEqual(hoistedDevJson);
expect(isoLongJson).toStrictEqual(hoistedLongJson);
expect(isoLong).toContain("│ Javier Blanco <http://jbgutierrez.info>\n");
});
test.concurrent("isolated linker matches hoisted: --filter in a workspace", async () => {
const isoMono = await setup("isolated", monorepoFiles);
const [isoJson, hoistedJson, isoText] = await Promise.all([
licensesJson(isoMono, "--filter", "foo"),
licensesJson(monoDir, "--filter", "foo"),
licensesText(isoMono, "--filter", "foo"),
]);
expect(isoJson).toStrictEqual(hoistedJson);
expect(isoText).toContain("├── [email protected] (dev)\n└── [email protected]\n");
});
test.concurrent("isolated linker: scoped transitive dependency is found through the store", async () => {
const dir = await setup("isolated", { "package.json": pkg({ dependencies: { "two-range-deps": "1.0.0" } }) });
expect(existsSync(join(dir, "node_modules", "@types", "is-number"))).toBeFalse();
expect(existsSync(join(dir, "node_modules", ".bun", "@[email protected]"))).toBeTrue();
expect(await licensesJson(dir)).toStrictEqual({
Unknown: [u("@types/is-number", "2.0.0"), u("no-deps", "1.1.0"), u("two-range-deps", "1.0.0")],
});
expect(pathsOf(await licensesEntries(dir), "@types/is-number")).toStrictEqual([
store(dir, "@[email protected]", "@types", "is-number"),
]);
});
test.concurrent("paths: isolated installs report the store directory", async () => {
const dir = await setup("isolated");
const entries = await licensesEntries(dir);
expect(Object.fromEntries(entries.map(entry => [entry.name, entry.paths]))).toStrictEqual({
"path-parse": [store(dir, "[email protected]", "path-parse")],
"resolve": [store(dir, "[email protected]", "resolve")],
"a-dep": [store(dir, "[email protected]", "a-dep")],
"no-deps": [store(dir, "[email protected]", "no-deps"), store(dir, "[email protected]", "no-deps")],
"one-dep": [store(dir, "[email protected]", "one-dep")],
});
expect(await licensesJson(dir)).toStrictEqual(fullJson);
});
test.concurrent("isolated linker: store entries with a peer hash suffix are matched", async () => {
const dir = await setup("isolated", {
"package.json": pkg({ dependencies: { "peer-deps-lvl0": "1.0.0" } }),
});
const storeEntries = readdirSync(join(dir, "node_modules", ".bun"));
expect(storeEntries.some(name => /^peer-deps-lvl[12]@1\.0\.0\+[0-9a-f]{16}$/.test(name))).toBeTrue();
expect(await licensesJson(dir)).toStrictEqual({
Unknown: [
u("no-deps", "1.0.0"),
u("peer-deps-lvl0", "1.0.0"),
u("peer-deps-lvl1", "1.0.0"),
u("peer-deps-lvl2", "1.0.0"),
],
});
const [lvl1Path] = pathsOf(await licensesEntries(dir), "peer-deps-lvl1")!;
expect(lvl1Path).toMatch(new RegExp("peer-deps-lvl1@1\\.0\\.0\\+[0-9a-f]{16}"));
expect(lvl1Path.startsWith(join(dir, "node_modules", ".bun"))).toBeTrue();
expect(existsSync(join(lvl1Path, "package.json"))).toBeTrue();
});
// pnpm 'should work with file protocol dependency' (fixtures/with-file-protocol): a license-less folder dep is listed as Unknown.
test.concurrent.each(["hoisted", "isolated"] as Linker[])("file: dependency is listed (%s)", async linker => {
const dir = await setup(linker, {
"package.json": pkg({ dependencies: { "no-deps": "1.0.0", "sub-dep": "file:./sub-dep" } }),
"sub-dep/package.json": JSON.stringify({ name: "sub-dep", version: "2.5.0" }),
});
expect(await licensesJson(dir)).toStrictEqual({
Unknown: [u("no-deps", "1.0.0"), u("sub-dep", "sub-dep")],
});
expect(pathsOf(await licensesEntries(dir), "sub-dep")).toStrictEqual([
linker === "hoisted" ? nm(dir, "sub-dep") : store(dir, "sub-dep@file+sub-dep", "sub-dep"),
]);
});
test.concurrent("--prod omits a file: dependency's devDependencies, like bun install --production", async () => {
const dir = await setup("hoisted", {
"package.json": pkg({ dependencies: { "no-deps": "1.0.0", "sub-dep": "file:./sub-dep" } }),
"sub-dep/package.json": JSON.stringify({
name: "sub-dep",
version: "2.5.0",
devDependencies: { "a-dep": "1.0.1" },
}),
});
expect(names(await licensesJson(dir))).toStrictEqual(["a-dep", "no-deps", "sub-dep"]);
expect(names(await licensesJson(dir, "--prod"))).toStrictEqual(["no-deps", "sub-dep"]);
});
test.concurrent.each(["hoisted", "isolated"] as Linker[])("link: dependency is not listed (%s)", async linker => {
const { packageDir } = await registry.createTestDir({
bunfigOpts: { linker },
files: {
"package.json": pkg({ dependencies: { "no-deps": "1.0.0", "linked": "link:linked" } }),
"linked/package.json": JSON.stringify({ name: "linked", version: "1.0.0", license: "MIT" }),
},
});
const env = { ...installEnv(packageDir), BUN_INSTALL_GLOBAL_DIR: join(packageDir, ".global") };
for (const [cmd, cwd] of [
[[bunExe(), "link"], join(packageDir, "linked")],
[[bunExe(), "install", "--linker", linker], packageDir],
] as const) {
await using proc = spawn({ cmd: [...cmd], env, cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);
}
expect(existsSync(join(packageDir, "node_modules", "linked", "package.json"))).toBeTrue();
await using proc = spawn({
cmd: [bunExe(), "pm", "licenses", "--json"],
env,
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stripPaths(JSON.parse(stdout))).toStrictEqual({ Unknown: [u("no-deps", "1.0.0")] });
expect(exitCode).toBe(0);
});
// pnpm license-scanner 'lists versions installed under different aliases'.
test.concurrent.each(["hoisted", "isolated"] as Linker[])(
"npm: alias is listed under the real name (%s)",
async linker => {
const dir = await setup(linker, {
"package.json": pkg({ dependencies: { "no-deps": "1.0.0", "nd2": "npm:[email protected]" } }),
});
expect(await licensesJson(dir)).toStrictEqual({ Unknown: [u("no-deps", "1.0.0", "1.0.1")] });
},
);
// pnpm 'path should be correct for workspaces' / 'filter outputs'; pnpm#5689 (same output from every directory of a monorepo).
test.concurrent("workspace root lists every member's dependencies; a member lists only its own closure", async () => {
expect(await licensesJson(monoDir)).toStrictEqual(monoJson);
expect(await licensesJson(join(monoDir, "packages", "bar"))).toStrictEqual(barJson);
expect(await licensesJson(join(monoDir, "packages", "foo"))).toStrictEqual(fooJson);
expect(await licensesText(monoDir)).toContain("├── [email protected] (dev)\n└── [email protected]\n");
const fromRoot = await licensesEntries(monoDir);
expect(pathsOf(fromRoot, "a-dep")).toStrictEqual([nm(monoDir, "a-dep")]);
expect(pathsOf(fromRoot, "resolve")).toStrictEqual([nm(monoDir, "resolve")]);
expect(pathsOf(await licensesEntries(monoDir, "--filter", "foo"), "a-dep")).toStrictEqual([nm(monoDir, "a-dep")]);
expect(pathsOf(await licensesEntries(join(monoDir, "packages", "foo")), "a-dep")).toStrictEqual([
nm(monoDir, "a-dep"),
]);
});
test.concurrent("(dev) from the root is unmarked when another member needs the package in production", async () => {
const dir = await setup("hoisted", {
...monorepoFiles,
"packages/foo/package.json": JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: { "no-deps": "1.0.0" },
devDependencies: { "a-dep": "1.0.1", "resolve": "1.9.0" },
}),
});
expect(normalizeBunSnapshot(await licensesText(dir))).toMatchInlineSnapshot(`
"MIT (2)
├── [email protected]
└── [email protected]
Unknown (2)
├── [email protected] (dev)
└── [email protected]"
`);
expect(await licensesText(join(dir, "packages", "foo"))).toContain("├── [email protected] (dev)\n└── [email protected]\n");
});
test.concurrent("--prod inside a workspace drops members' devDependencies", async () => {
expect(await licensesJson(monoDir, "--prod")).toStrictEqual({
MIT: fullJson.MIT,
Unknown: [u("no-deps", "1.0.0")],
});
expect(await licensesJson(join(monoDir, "packages", "foo"), "--prod")).toStrictEqual({
Unknown: [u("no-deps", "1.0.0")],
});
});
test.concurrent("--filter selects workspaces from any directory", async () => {
const bar = join(monoDir, "packages", "bar");
const [union, star, all, notFoo, starNotFoo, glob, parentGlob, packagesGlob, rootOnly, fooText] = await Promise.all(
[
licensesJson(monoDir, "-F", "foo", "-F", "bar"),
licensesJson(monoDir, "--filter", "*"),
licensesJson(monoDir),
licensesJson(monoDir, "--filter", "!foo"),
licensesJson(monoDir, "--filter", "*", "--filter", "!foo"),
licensesJson(monoDir, "--filter", "b*"),
licensesJson(bar, "--filter", "../*"),
licensesJson(monoDir, "--filter", "./packages/*"),
licensesText(monoDir, "--filter", "mono"),
licensesText(monoDir, "--filter", "foo"),
],
);
expect(await licensesJson(monoDir, "--filter", "foo")).toStrictEqual(fooJson);
expect(await licensesJson(monoDir, "--filter", "bar")).toStrictEqual(barJson);
expect(await licensesJson(monoDir, "--filter", "./packages/bar")).toStrictEqual(barJson);
expect(await licensesJson(bar, "--filter", "foo")).toStrictEqual(fooJson);
expect(await licensesJson(bar, "--filter", "./")).toStrictEqual(barJson);
expect(union).toStrictEqual(monoJson);
expect(names(union)).toStrictEqual(monoNames);
expect(star).toStrictEqual(all);
expect(all).toStrictEqual(monoJson);
expect(notFoo).toStrictEqual(barJson);
expect(starNotFoo).toStrictEqual(barJson);
expect(glob).toStrictEqual(barJson);
expect(parentGlob).toStrictEqual(monoJson);
expect(packagesGlob).toStrictEqual(monoJson);
expectEmptyText(rootOnly, 0);
expect(normalizeBunSnapshot(fooText)).toMatchInlineSnapshot(`
"Unknown (2)
├── [email protected] (dev)
└── [email protected]"
`);
expect(await licensesJson(monoDir, "--filter", "foo", "--prod")).toStrictEqual({
Unknown: [u("no-deps", "1.0.0")],
});
expect(await licensesJson(monoDir, "--filter", "foo", "--dev")).toStrictEqual({ Unknown: [u("a-dep", "1.0.1")] });
expect(await licensesJson(monoDir, "--filter", "bar", "--dev")).toStrictEqual({});
});
test.concurrent("--filter selecting the root lists only the root's own dependencies", async () => {
const dir = await setup("hoisted", {
...monorepoFiles,
"package.json": JSON.stringify({
name: "mono",
private: true,
workspaces: ["packages/*"],
dependencies: { "one-dep": "1.0.0" },
}),
});
expect(await licensesJson(dir, "--filter", "mono")).toStrictEqual({
Unknown: [u("no-deps", "1.0.1"), u("one-dep", "1.0.0")],
});
expect(await licensesJson(dir)).toStrictEqual({
MIT: fullJson.MIT,
Unknown: [u("a-dep", "1.0.1"), u("no-deps", "1.0.0", "1.0.1"), u("one-dep", "1.0.0")],
});
});
test.concurrent("--filter matching nothing is an error", async () => {
const [stdout, stderr, exitCode] = await licenses(monoDir, "--filter", "nope", "--json");
expect(stdout).toBe("");
expect(normalizeBunSnapshot(stderr)).toMatchInlineSnapshot(
`"error: No workspace packages matched the filter "nope""`,
);
expect(exitCode).toBe(1);
});
test.concurrent("--filter patterns that match nothing are reported when other patterns match", async () => {
const [stdout, stderr, exitCode] = await licenses(monoDir, "--filter", "foo", "--filter", "nomatch", "--json");
expect(normalizeBunSnapshot(stderr)).toMatchInlineSnapshot(
`"warn: No workspace packages matched the filter "nomatch""`,
);
expect(stripPaths(JSON.parse(stdout))).toStrictEqual(fooJson);
expect(exitCode).toBe(0);
const [text, textStderr, textExit] = await licenses(
monoDir,
"--filter",
"nomatch",
"--filter",
"alsonone",
"-F",
"bar",
);
expect(normalizeBunSnapshot(textStderr)).toMatchInlineSnapshot(
`"warn: No workspace packages matched the filters "nomatch", "alsonone""`,
);
expect(normalizeBunSnapshot(text)).toMatchInlineSnapshot(`
"MIT (2)
├── [email protected]
└── [email protected]"
`);
expect(textExit).toBe(0);
});
test.concurrent("--filter is rejected by other pm subcommands and works outside a monorepo", async () => {
const [stdout, stderr, exitCode] = await pm(hoistedDir, "ls", "--filter", "foo");
expect(stdout).toBe("");
expect(stderr).toContain("--filter is only supported by `bun pm licenses`");
expect(exitCode).toBe(1);
expect(await licensesJson(hoistedDir, "--filter", "licenses-fixture")).toStrictEqual(fullJson);
});
test.concurrent("nothing to list prints what was checked and how long it took / {}", async () => {
const dir = await setup("hoisted", { "package.json": pkg({ devDependencies: { "no-deps": "1.0.0" } }) });
const [stdout, stderr, exitCode] = await licenses(dir, "--prod");
expectEmptyText(stdout, 0);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
const [json, jsonStderr, jsonExit] = await licenses(dir, "--prod", "--json");
expect(json).toBe("{}\n");
expect(jsonStderr).toBe("");
expect(jsonExit).toBe(0);
expect(await licensesJson(dir)).toStrictEqual({ Unknown: [u("no-deps", "1.0.0")] });
});
test.concurrent("nothing to list counts the lockfile packages that were checked but not installed", async () => {
const dir = await setup();
for (const name of ["a-dep", "no-deps", "one-dep", "path-parse", "resolve"])
rmSync(nm(dir, name), { recursive: true });
const [stdout, stderr, exitCode] = await licenses(dir);
expectEmptyText(stdout, 6);
expect(normalizeBunSnapshot(stderr)).toMatchInlineSnapshot(`
"warn: 6 packages in bun.lock are not installed and were skipped
note: run 'bun install' first"
`);
expect(exitCode).toBe(0);
const [json, jsonStderr, jsonExit] = await licenses(dir, "--json");
expect(json).toBe("{}\n");
expect(normalizeBunSnapshot(jsonStderr)).toBe(
`warn: 6 packages in bun.lock are not installed and were skipped\n${MISSING_NOTE}`,
);
expect(jsonExit).toBe(0);
});
test.concurrent("--silent still prints the listing but no diagnostics", async () => {
const dir = await setup();
rmSync(nm(dir, "path-parse"), { recursive: true });
const [[loud, loudStderr, loudExit], [quiet, quietStderr, quietExit]] = await Promise.all([
licenses(dir),
licenses(dir, "--silent"),
]);
expect(normalizeBunSnapshot(loudStderr)).toBe(
`warn: 1 package in bun.lock is not installed and was skipped\n${MISSING_NOTE}`,
);
expect(quietStderr).toBe("");
expect(quiet).toBe(loud);
expect(normalizeBunSnapshot(quiet)).toMatchInlineSnapshot(`
"MIT (1)
└── [email protected]
Unknown (4)
├── [email protected] (dev)
├── [email protected]
├── [email protected]
└── [email protected]"
`);
expect([loudExit, quietExit]).toStrictEqual([0, 0]);
const [json, jsonStderr, jsonExit] = await licenses(dir, "--silent", "--json");
expect(jsonStderr).toBe("");
expect(stripPaths(JSON.parse(json))).toStrictEqual({ MIT: [fullJson.MIT[1]], Unknown: fullJson.Unknown });
expect(jsonExit).toBe(0);
const [partial, partialStderr, partialExit] = await licenses(
monoDir,
"--silent",
"-F",
"foo",
"-F",
"nomatch",
"--json",
);
expect(partialStderr).toBe("");
expect(stripPaths(JSON.parse(partial))).toStrictEqual(fooJson);
expect(partialExit).toBe(0);
});
test.concurrent("--silent suppresses every error but keeps the exit code", async () => {
const noNodeModules = await setup();
rmSync(nm(noNodeModules), { recursive: true });
const noLockfile = (await registry.createTestDir({ files: { "package.json": fixturePackageJson } })).packageDir;
const cases: [string, string[]][] = [
[noNodeModules, []],
[noLockfile, []],
[hoistedDir, ["--dev", "--prod"]],
[monoDir, ["--filter", "nomatch"]],
[monoDir, ["--filter", "nomatch", "--filter", "alsonone", "--json"]],
[hoistedDir, ["bogus"]],
[hoistedDir, ["list", "extra"]],
];
for (const [cwd, args] of cases) {
const [stdout, stderr, exitCode] = await licenses(cwd, "--silent", ...args);
expect({ args, stdout, stderr, exitCode }).toStrictEqual({ args, stdout: "", stderr: "", exitCode: 1 });
}
});
test.concurrent("licenses list / ls aliases", async () => {
const [[plain, , plainExit], [list, , listExit], [ls, , lsExit]] = await Promise.all([
licenses(hoistedDir),
licenses(hoistedDir, "list"),
licenses(hoistedDir, "ls"),
]);
expect(plain).toContain("MIT (2)");
expect(list).toBe(plain);
expect(ls).toBe(plain);
expect([plainExit, listExit, lsExit]).toStrictEqual([0, 0, 0]);
const [stdout, stderr, exitCode] = await licenses(hoistedDir, "bogus");
expect(stdout).toBe("");
expect(normalizeBunSnapshot(stderr)).toMatchInlineSnapshot(`
"error: unknown subcommand "bogus" for bun pm licenses
note: did you mean 'bun pm licenses list'?"
`);
expect(exitCode).toBe(1);
});
test.concurrent("licenses list / ls reject extra arguments", async () => {
for (const subcommand of ["list", "ls"]) {
const [stdout, stderr, exitCode] = await licenses(hoistedDir, subcommand, "extra");
expect(stdout).toBe("");
expect(normalizeBunSnapshot(stderr)).toBe(`error: bun pm licenses ${subcommand} does not take arguments`);
expect(exitCode).toBe(1);
}
});
test.concurrent("missing lockfile", async () => {
const { packageDir } = await registry.createTestDir({ files: { "package.json": fixturePackageJson } });
const [stdout, stderr, exitCode] = await licenses(packageDir);
expect(stdout).toBe("");
expect(stderr).toContain("error: missing lockfile");
expect(stderr).toContain(MISSING_NOTE);
expect(exitCode).toBe(1);
});
test.concurrent("missing node_modules", async () => {
const { packageDir } = await registry.createTestDir({ files: { "package.json": fixturePackageJson } });
await install(packageDir, "hoisted", "--lockfile-only");
for (const args of [[], ["--json"], ["list"]]) {
const [stdout, stderr, exitCode] = await licenses(packageDir, ...args);
expect(stdout).toBe("");
expect(normalizeBunSnapshot(stderr)).toBe(`error: node_modules not found, nothing to list\n${MISSING_NOTE}`);
expect(exitCode).toBe(1);
}
});
// pnpm#5702: a package that cannot be read must not fail the whole listing; the omission is reported instead of silent.
test.concurrent(
"unparsable package.json is reported as Unknown, missing ones are omitted with a warning",
async () => {
const dir = await setup();
overwriteInstalledManifest(dir, "resolve", "{ not json");
rmSync(join(dir, "node_modules", "one-dep"), { recursive: true, force: true });
const [stdout, stderr, exitCode] = await licenses(dir, "--json");
expect(normalizeBunSnapshot(stderr)).toMatchInlineSnapshot(`
"warn: 2 packages in bun.lock are not installed and were skipped
note: run 'bun install' first"
`);
expect(stripPaths(JSON.parse(stdout))).toStrictEqual({
MIT: [fullJson.MIT[0]],
Unknown: [u("a-dep", "1.0.1"), u("no-deps", "1.0.0"), u("resolve", "1.9.0")],
});
expect(exitCode).toBe(0);
},
);
// pnpm#8589: the lockfile's resolvable tree hoists the dev subtree's [email protected], but `--production` installs [email protected] there.
const outHoistedFixture = {
"package.json": pkg({
dependencies: { "uses-a-dep-10": "1.0.0" },
devDependencies: { "uses-a-dep-9": "1.0.0" },
}),
};
test.concurrent("pnpm#8589: --prod after `bun install --production` finds packages hoisted differently", async () => {
const dir = await setupProductionInstall(outHoistedFixture);
expect(JSON.parse(readFileSync(join(dir, "node_modules", "a-dep", "package.json"), "utf8")).version).toBe("1.0.10");
expect(existsSync(join(dir, "node_modules", "uses-a-dep-10", "node_modules"))).toBeFalse();
expect(await licensesJson(dir, "--prod")).toStrictEqual({
Unknown: [u("a-dep", "1.0.10"), u("uses-a-dep-10", "1.0.0")],
});
expect(pathsOf(await licensesEntries(dir, "--prod"), "a-dep")).toStrictEqual([nm(dir, "a-dep")]);
});
test.concurrent("pnpm#8589: a different version at the tree path is not misattributed", async () => {
const dir = await setupProductionInstall(outHoistedFixture);
patchInstalledManifest(dir, "a-dep", { license: "MIT" });
const [stdout, stderr, exitCode] = await licenses(dir, "--json");
expect(normalizeBunSnapshot(stderr)).toMatchInlineSnapshot(`
"warn: 2 packages in bun.lock are not installed and were skipped
note: run 'bun install' first"
`);
expect(stripPaths(JSON.parse(stdout))).toStrictEqual({
MIT: [{ name: "a-dep", versions: ["1.0.10"], license: "MIT" }],
Unknown: [u("uses-a-dep-10", "1.0.0")],
});
expect(exitCode).toBe(0);
});
// The lockfile nests bundled deps under their parent, which is also where the tarball unpacks them.
test.concurrent.each(["hoisted", "isolated"] as Linker[])(
"bundled dependencies are listed from inside their parent (%s)",
async linker => {
const dir = await setup(linker, { "package.json": pkg({ dependencies: { "bundled-1": "1.0.0" } }) });
expect(existsSync(join(dir, "node_modules", "bundled-1", "node_modules", "no-deps", "package.json"))).toBeTrue();
expect(await licensesJson(dir)).toStrictEqual({
Unknown: [u("bundled-1", "1.0.0"), u("no-deps", "1.0.0")],
});
const entries = await licensesEntries(dir);
if (linker === "hoisted") {
expect(pathsOf(entries, "bundled-1")).toStrictEqual([nm(dir, "bundled-1")]);
expect(pathsOf(entries, "no-deps")).toStrictEqual([nm(dir, "bundled-1", "node_modules", "no-deps")]);
} else {
expect(pathsOf(entries, "bundled-1")).toStrictEqual([store(dir, "[email protected]", "bundled-1")]);
const noDepsPaths = pathsOf(entries, "no-deps")!;
expect(noDepsPaths).toBeArrayOfSize(1);
expect(noDepsPaths[0]).toEndWith(join("bundled-1", "node_modules", "no-deps"));
}
},
);
// pnpm#8739: git dependencies are looked up the same way the installer wrote them.
test.concurrent.each(["hoisted", "isolated"] as Linker[])("git dependency is listed (%s)", async linker => {
using repoDir = await gitRepo({ name: "git-pkg", version: "3.0.0", license: "ISC" });
const dir = await setup(linker, {
"package.json": pkg({ dependencies: { "no-deps": "1.0.0", "git-pkg": `git+${pathToFileURL(String(repoDir))}` } }),
});
const parsed = await licensesJsonRaw(dir);
expect(Object.keys(parsed)).toStrictEqual(["ISC", "Unknown"]);
const [gitPath] = pathsOf(Object.values(parsed).flat(), "git-pkg")!;
expect(stripPaths(parsed).ISC).toStrictEqual([
{ name: "git-pkg", versions: [expect.stringContaining("git+file://")], license: "ISC" },
]);
expect(parsed.Unknown).toStrictEqual([u("no-deps", "1.0.0")]);
if (linker === "hoisted") {
expect(gitPath).toBe(nm(dir, "git-pkg"));
} else {
expect(gitPath.startsWith(join(dir, "node_modules", ".bun") + sep)).toBeTrue();
expect(gitPath).toEndWith(join("node_modules", "git-pkg"));
expect(gitPath).not.toBe(nm(dir, "git-pkg"));
}
});
test.concurrent("npm version sorts before a git resolution of the same name in text and --json", async () => {
using repoDir = await gitRepo({ name: "no-deps", version: "9.9.9" });
const dir = await setup("hoisted", {
"package.json": pkg({ dependencies: { "no-deps": "1.0.0", "nd": `git+${pathToFileURL(String(repoDir))}` } }),
});
const [stdout, stderr, exitCode] = await licenses(dir);
expect(normalizeBunSnapshot(stdout).replace(/git\+file:\/\/\S+/, "git+file://<repo>")).toMatchInlineSnapshot(`
"Unknown (2)
├── [email protected]
└── no-deps@git+file://<repo>"
`);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
const [entry] = await licensesEntries(dir);
expect(entry.name).toBe("no-deps");
expect(entry.versions).toStrictEqual(["1.0.0", expect.stringContaining("git+file://")]);
expect(entry.paths).toStrictEqual([nm(dir, "no-deps"), nm(dir, "nd")]);
});
test.concurrent.each(["hoisted", "isolated"] as Linker[])("tarball dependencies are listed (%s)", async linker => {
const remoteUrl = `${registry.registryUrl()}a-dep/-/a-dep-1.0.1.tgz`;
const { packageDir: dir } = await registry.createTestDir({
bunfigOpts: { linker },
files: {
"package.json": pkg({
dependencies: { "no-deps": "1.0.0", "pp": "file:./path-parse-1.0.6.tgz", "ad": remoteUrl },
}),
},
});
copyFileSync(join(registry.packagesPath, "path-parse", "path-parse-1.0.6.tgz"), join(dir, "path-parse-1.0.6.tgz"));
await install(dir, linker);
const parsed = await licensesJsonRaw(dir);
const entries = Object.values(parsed).flat();
const [ppPath] = pathsOf(entries, "path-parse")!;
const [adPath] = pathsOf(entries, "a-dep")!;
expect(stripPaths(parsed)).toStrictEqual({
MIT: [{ ...fullJson.MIT[0], versions: ["./path-parse-1.0.6.tgz"] }],
Unknown: [u("a-dep", remoteUrl), u("no-deps", "1.0.0")],
});
if (linker === "hoisted") {
expect(ppPath).toBe(nm(dir, "pp"));
expect(adPath).toBe(nm(dir, "ad"));
} else {
expect(ppPath).toBe(store(dir, "path-parse@.+path-parse-1.0.6.tgz", "path-parse"));
expect(adPath).toBe(store(dir, `a-dep@${remoteUrl.replace(/[/:]/g, "+")}`, "a-dep"));
}
});
test.concurrent("--omit=dev and bunfig install.production behave like --prod", async () => {
expect(await licensesJson(hoistedDir, "--omit=dev")).toStrictEqual(prodJson);
expect(await licensesText(hoistedDir, "--omit=dev")).not.toContain("(dev)");
expect(await licensesText(hoistedDir, "--omit", "dev")).toBe(await licensesText(hoistedDir, "--prod"));
const dir = await setup();
const bunfig = readFileSync(join(dir, "bunfig.toml"), "utf8");
expect(bunfig).toStartWith("[install]\n");
writeFileSync(join(dir, "bunfig.toml"), bunfig.replace("[install]\n", "[install]\nproduction = true\n"));
expect(await licensesJson(dir)).toStrictEqual(prodJson);
const text = await licensesText(dir);
expect(text).not.toContain("a-dep");
expect(text).not.toContain("(dev)");
});
test.concurrent("--omit=optional drops the optionalDependencies closure", async () => {
const dir = await setup("hoisted", {
"package.json": pkg({ dependencies: { "no-deps": "1.0.0" }, optionalDependencies: { "one-dep": "1.0.0" } }),
});
expect(await licensesJson(dir)).toStrictEqual({ Unknown: [u("no-deps", "1.0.0", "1.0.1"), u("one-dep", "1.0.0")] });
expect(await licensesJson(dir, "--omit=optional")).toStrictEqual({ Unknown: [u("no-deps", "1.0.0")] });
expect(normalizeBunSnapshot(await licensesText(dir, "--omit=optional"))).toMatchInlineSnapshot(`
"Unknown (1)
└── [email protected]"
`);
});
test.concurrent("packages reachable only through peerDependencies are listed; --omit=peer drops them", async () => {
const dir = await setup("hoisted", { "package.json": pkg({ peerDependencies: { "no-deps": "1.0.0" } }) });
expect(existsSync(nm(dir, "no-deps", "package.json"))).toBeTrue();
expect(await licensesJson(dir)).toStrictEqual({ Unknown: [u("no-deps", "1.0.0")] });
expect(normalizeBunSnapshot(await licensesText(dir))).toMatchInlineSnapshot(`
"Unknown (1)
└── [email protected]"
`);
expectEmptyText(await licensesText(dir, "--omit=peer"), 0);
expect(await licensesJson(dir, "--omit=peer")).toStrictEqual({});
const transitive = await setup("hoisted", { "package.json": pkg({ dependencies: { "has-peer": "1.0.0" } }) });
expect(await licensesJson(transitive)).toStrictEqual({
Unknown: [u("has-peer", "1.0.0"), u("peer-no-deps", "1.0.1")],
});
expect(await licensesJson(transitive, "--omit=peer")).toStrictEqual({ Unknown: [u("has-peer", "1.0.0")] });
});
test.concurrent("hoisted linker: scoped packages at their tree location and found by the disk scan", async () => {
const dir = await setup("hoisted", { "package.json": pkg({ dependencies: { "two-range-deps": "1.0.0" } }) });
const expected = { Unknown: [u("@types/is-number", "2.0.0"), u("no-deps", "1.1.0"), u("two-range-deps", "1.0.0")] };
expect(await licensesJson(dir)).toStrictEqual(expected);
expect(pathsOf(await licensesEntries(dir), "@types/is-number")).toStrictEqual([nm(dir, "@types", "is-number")]);
renameSync(nm(dir, "@types", "is-number"), nm(dir, "@types", "renamed"));
expect(await licensesJson(dir)).toStrictEqual(expected);
expect(pathsOf(await licensesEntries(dir), "@types/is-number")).toStrictEqual([nm(dir, "@types", "renamed")]);
mkdirSync(nm(dir, "two-range-deps", "node_modules"));
renameSync(nm(dir, "@types"), nm(dir, "two-range-deps", "node_modules", "@types"));
expect(await licensesJson(dir)).toStrictEqual(expected);
expect(pathsOf(await licensesEntries(dir), "@types/is-number")).toStrictEqual([
nm(dir, "two-range-deps", "node_modules", "@types", "renamed"),
]);
});
test.concurrent("Auto linker resolves to isolated for a workspace project", async () => {
const { packageDir: dir } = await registry.createTestDir({ bunfigOpts: {}, files: monorepoFiles });
await using proc = spawn({
cmd: [bunExe(), "install"],
env: installEnv(dir),
cwd: dir,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
});
const [installStderr, installExit] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(installStderr).not.toContain("error:");
expect(installExit).toBe(0);
expect(existsSync(nm(dir, ".bun", "[email protected]"))).toBeTrue();
const parsed = await licensesJsonRaw(dir);
expect(
Object.fromEntries(Object.values(parsed).flatMap(entries => entries.map(e => [e.name, e.paths]))),
).toStrictEqual({
"path-parse": [store(dir, "[email protected]", "path-parse")],
"resolve": [store(dir, "[email protected]", "resolve")],
"a-dep": [store(dir, "[email protected]", "a-dep")],
"no-deps": [store(dir, "[email protected]", "no-deps")],
});
expect(stripPaths(parsed)).toStrictEqual(monoJson);
});
test.concurrent("one missing package is reported in the singular", async () => {
const dir = await setup();
rmSync(nm(dir, "a-dep"), { recursive: true });
const [stdout, stderr, exitCode] = await licenses(dir, "--json");
expect(normalizeBunSnapshot(stderr)).toMatchInlineSnapshot(`
"warn: 1 package in bun.lock is not installed and was skipped
note: run 'bun install' first"
`);
expect(stripPaths(JSON.parse(stdout))).toStrictEqual(prodJson);
expect(exitCode).toBe(0);
});
test.concurrent("hoisted linker falls back to a node_modules/.bun store entry", async () => {
const dir = await setup();
mkdirSync(store(dir, "[email protected]"), { recursive: true });
renameSync(nm(dir, "a-dep"), store(dir, "[email protected]", "a-dep"));
const [stdout, stderr, exitCode] = await licenses(dir, "--json");
expect(stderr).toBe("");
const parsed: Record<string, LicenseEntry[]> = JSON.parse(stdout);
expect(pathsOf(Object.values(parsed).flat(), "a-dep")).toStrictEqual([store(dir, "[email protected]", "a-dep")]);
expect(stripPaths(parsed)).toStrictEqual(fullJson);
expect(exitCode).toBe(0);
});
test.concurrent("an installed manifest without a version is listed under the lockfile version", async () => {
const dir = await setup();
patchInstalledManifest(dir, "resolve", { version: undefined });
const [stdout, stderr, exitCode] = await licenses(dir, "--json");
expect(stderr).toBe("");
const parsed: Record<string, LicenseEntry[]> = JSON.parse(stdout);
expect(pathsOf(parsed.MIT, "resolve")).toStrictEqual([nm(dir, "resolve")]);
expect(stripPaths(parsed)).toStrictEqual(fullJson);
expect(exitCode).toBe(0);
});
test.concurrent("author objects: name/email/url combinations; an empty object omits the author", async () => {
const dir = await setup();
patchInstalledManifest(dir, "a-dep", {
author: { name: "Ann", email: "[email protected]", url: "https://ann.example" },
});
patchInstalledManifest(dir, "one-dep", { author: { email: "[email protected]" } });
patchInstalledManifest(dir, "one-dep/node_modules/no-deps", { author: { url: "https://nd.example" } });
patchInstalledManifest(dir, "resolve", { author: {} });
expect(await licensesJson(dir)).toStrictEqual({
MIT: [
fullJson.MIT[0],
{
name: "resolve",
versions: ["1.9.0"],
license: "MIT",
homepage: resolveHomepage,
description: resolveDescription,
},
],
Unknown: [
{ ...u("a-dep", "1.0.1"), author: "Ann <[email protected]> (https://ann.example)" },
{ ...u("no-deps", "1.0.0", "1.0.1"), author: "(https://nd.example)" },
{ ...u("one-dep", "1.0.0"), author: "<[email protected]>" },
],
});
expect(normalizeBunSnapshot(await licensesText(dir, "--long"))).toMatchInlineSnapshot(`
"MIT (2)
├── [email protected]
│ Javier Blanco <http://jbgutierrez.info>
│ Node.js path.parse() ponyfill
│ https://github.com/jbgutierrez/path-parse#readme
└── [email protected]
resolve like require.resolve() on behalf of files asynchronously and synchronously
git://github.com/browserify/resolve.git
Unknown (4)
├── [email protected] (dev)
│ Ann <[email protected]> (https://ann.example)
├── [email protected]
├── [email protected]
│ (https://nd.example)
└── [email protected]
<[email protected]>"
`);
});
test.concurrent("--filter matching nothing lists every pattern", async () => {
const [stdout, stderr, exitCode] = await licenses(monoDir, "--filter", "nope", "--filter", "nada");
expect(stdout).toBe("");
expect(normalizeBunSnapshot(stderr)).toMatchInlineSnapshot(
`"error: No workspace packages matched the filters "nope", "nada""`,
);
expect(exitCode).toBe(1);
});
test.concurrent("--json is pretty-printed with inline versions and paths arrays", async () => {
const [stdout, stderr, exitCode] = await licenses(hoistedDir, "--json");
expect(stderr).toBe("");
const dirInJson = JSON.stringify(hoistedDir).slice(1, -1);
expect(stdout.split(dirInJson).length).toBe(7);
expect(stdout.replaceAll(dirInJson, "<dir>").replaceAll("\\\\", "/")).toMatchInlineSnapshot(`
"{
"MIT": [
{
"name": "path-parse",
"versions": ["1.0.6"],
"paths": ["<dir>/node_modules/path-parse"],
"license": "MIT",
"author": "Javier Blanco <http://jbgutierrez.info>",
"description": "Node.js path.parse() ponyfill",
"homepage": "https://github.com/jbgutierrez/path-parse#readme"
},
{
"name": "resolve",
"versions": ["1.9.0"],
"paths": ["<dir>/node_modules/resolve"],
"license": "MIT",
"author": "James Halliday <[email protected]> (http://substack.net)",
"description": "resolve like require.resolve() on behalf of files asynchronously and synchronously",
"homepage": "git://github.com/browserify/resolve.git"
}
],
"Unknown": [
{
"name": "a-dep",
"versions": ["1.0.1"],
"paths": ["<dir>/node_modules/a-dep"],
"license": "Unknown"
},
{
"name": "no-deps",
"versions": ["1.0.0", "1.0.1"],
"paths": ["<dir>/node_modules/no-deps", "<dir>/node_modules/one-dep/node_modules/no-deps"],
"license": "Unknown"
},
{
"name": "one-dep",
"versions": ["1.0.0"],
"paths": ["<dir>/node_modules/one-dep"],
"license": "Unknown"
}
]
}
"
`);
expect(exitCode).toBe(0);
});
test.concurrent("a bun.lockb project is read without writing bun.lock or touching bun.lockb", async () => {
const { packageDir: dir } = await registry.createTestDir({
bunfigOpts: { linker: "hoisted", saveTextLockfile: false },
files: { "package.json": fixturePackageJson },
});
await install(dir, "hoisted");
expect(existsSync(join(dir, "bun.lock"))).toBeFalse();
const lockbBefore = readFileSync(join(dir, "bun.lockb"));
const packageJsonBefore = readFileSync(join(dir, "package.json"));
expect(await licensesJson(dir)).toStrictEqual(fullJson);
expect(await licensesText(dir)).toContain("MIT (2)\n");
expect(existsSync(join(dir, "bun.lock"))).toBeFalse();
expect(readFileSync(join(dir, "bun.lockb")).equals(lockbBefore)).toBeTrue();
expect(readFileSync(join(dir, "package.json")).equals(packageJsonBefore)).toBeTrue();
});
test.concurrent("bun pm help lists licenses and its flags", async () => {
for (const args of [[], ["--help"]]) {
const [stdout, stderr, exitCode] = await pm(hoistedDir, ...args);
const start = stdout.indexOf("bun pm licenses");
const end = stdout.indexOf("bun pm whoami");
expect(start).toBeGreaterThan(-1);
expect(end).toBeGreaterThan(start);
const block = stdout.slice(start, end);
for (const flag of ["--json", "--prod", "--dev", "--long", "--filter"]) expect(block).toContain(flag);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
}
});
});