mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
Implement catalogs in bun install (#19809)
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Jarred-Sumner <Jarred-Sumner@users.noreply.github.com>
This commit is contained in:
@@ -20,38 +20,16 @@ error: dependencies expects a map of specifiers, e.g.
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should report error on invalid format for optionalDependencies 1`] = `
|
||||
"1 | {"name":"foo","version":"0.0.1","optionalDependencies":"bar"}
|
||||
^
|
||||
error: optionalDependencies expects a map of specifiers, e.g.
|
||||
"optionalDependencies": {
|
||||
<green>"bun"<r>: <green>"latest"<r>
|
||||
}
|
||||
at [dir]/package.json:1:33
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should report error on invalid format for workspaces 1`] = `
|
||||
"1 | {"name":"foo","version":"0.0.1","workspaces":{"packages":{"bar":true}}}
|
||||
^
|
||||
error: Workspaces expects an array of strings, e.g.
|
||||
"workspaces": [
|
||||
"path/to/package"
|
||||
]
|
||||
at [dir]/package.json:1:33
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should report error on duplicated workspace packages 1`] = `
|
||||
"1 | {"name":"moo","version":"0.0.3"}
|
||||
^
|
||||
error: Workspace name "moo" already exists
|
||||
at [dir]/baz/package.json:1:9
|
||||
|
||||
1 | {"name":"moo","version":"0.0.2"}
|
||||
^
|
||||
note: Package name is also declared here
|
||||
at [dir]/bar/package.json:1:9
|
||||
^
|
||||
error: "workspaces.packages" expects an array of strings, e.g.
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"path/to/package"
|
||||
]
|
||||
}
|
||||
at [dir]/package.json:1:58
|
||||
"
|
||||
`;
|
||||
|
||||
|
||||
@@ -433,8 +433,8 @@ exports[`should sort overrides before comparing 1`] = `
|
||||
},
|
||||
},
|
||||
"overrides": {
|
||||
"what-bin": "1.0.0",
|
||||
"no-deps": "2.0.0",
|
||||
"what-bin": "1.0.0",
|
||||
},
|
||||
"packages": {
|
||||
"no-deps": ["no-deps@2.0.0", "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz", {}, "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g=="],
|
||||
@@ -470,9 +470,9 @@ exports[`should include unused resolutions in the lockfile 1`] = `
|
||||
},
|
||||
},
|
||||
"overrides": {
|
||||
"what-bin": "1.0.0",
|
||||
"no-deps": "2.0.0",
|
||||
"jquery": "4.0.0",
|
||||
"no-deps": "2.0.0",
|
||||
"what-bin": "1.0.0",
|
||||
},
|
||||
"packages": {
|
||||
"no-deps": ["no-deps@2.0.0", "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz", {}, "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g=="],
|
||||
|
||||
103
test/cli/install/__snapshots__/catalogs.test.ts.snap
Normal file
103
test/cli/install/__snapshots__/catalogs.test.ts.snap
Normal file
@@ -0,0 +1,103 @@
|
||||
// Bun Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`basic detect changes (bun.lock) 1`] = `
|
||||
"{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "catalog-basic-2",
|
||||
},
|
||||
"packages/pkg1": {
|
||||
"name": "pkg1",
|
||||
"dependencies": {
|
||||
"a-dep": "catalog:a",
|
||||
"no-deps": "catalog:",
|
||||
},
|
||||
},
|
||||
},
|
||||
"catalog": {
|
||||
"no-deps": "2.0.0",
|
||||
},
|
||||
"catalogs": {
|
||||
"a": {
|
||||
"a-dep": "1.0.1",
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"a-dep": ["a-dep@1.0.1", "http://localhost:1234/a-dep/-/a-dep-1.0.1.tgz", {}, "sha512-6nmTaPgO2U/uOODqOhbjbnaB4xHuZ+UB7AjKUA3g2dT4WRWeNxgp0dC8Db4swXSnO5/uLLUdFmUJKINNBO/3wg=="],
|
||||
|
||||
"no-deps": ["no-deps@2.0.0", "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz", {}, "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g=="],
|
||||
|
||||
"pkg1": ["pkg1@workspace:packages/pkg1"],
|
||||
}
|
||||
}
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`basic detect changes (bun.lock) 2`] = `
|
||||
"{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "catalog-basic-2",
|
||||
},
|
||||
"packages/pkg1": {
|
||||
"name": "pkg1",
|
||||
"dependencies": {
|
||||
"a-dep": "catalog:a",
|
||||
"no-deps": "catalog:",
|
||||
},
|
||||
},
|
||||
},
|
||||
"catalog": {
|
||||
"no-deps": "1.0.0",
|
||||
},
|
||||
"catalogs": {
|
||||
"a": {
|
||||
"a-dep": "1.0.1",
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"a-dep": ["a-dep@1.0.1", "http://localhost:1234/a-dep/-/a-dep-1.0.1.tgz", {}, "sha512-6nmTaPgO2U/uOODqOhbjbnaB4xHuZ+UB7AjKUA3g2dT4WRWeNxgp0dC8Db4swXSnO5/uLLUdFmUJKINNBO/3wg=="],
|
||||
|
||||
"no-deps": ["no-deps@1.0.0", "http://localhost:1234/no-deps/-/no-deps-1.0.0.tgz", {}, "sha512-v4w12JRjUGvfHDUP8vFDwu0gUWu04j0cv9hLb1Abf9VdaXu4XcrddYFTMVBVvmldKViGWH7jrb6xPJRF0wq6gw=="],
|
||||
|
||||
"pkg1": ["pkg1@workspace:packages/pkg1"],
|
||||
}
|
||||
}
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`basic detect changes (bun.lock) 3`] = `
|
||||
"{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "catalog-basic-2",
|
||||
},
|
||||
"packages/pkg1": {
|
||||
"name": "pkg1",
|
||||
"dependencies": {
|
||||
"a-dep": "catalog:a",
|
||||
"no-deps": "catalog:",
|
||||
},
|
||||
},
|
||||
},
|
||||
"catalog": {
|
||||
"no-deps": "1.0.0",
|
||||
},
|
||||
"catalogs": {
|
||||
"a": {
|
||||
"a-dep": "1.0.10",
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"a-dep": ["a-dep@1.0.10", "http://localhost:1234/a-dep/-/a-dep-1.0.10.tgz", {}, "sha512-NeQ6Ql9jRW8V+VOiVb+PSQAYOvVoSimW+tXaR0CoJk4kM9RIk/XlAUGCsNtn5XqjlDO4hcH8NcyaL507InevEg=="],
|
||||
|
||||
"no-deps": ["no-deps@1.0.0", "http://localhost:1234/no-deps/-/no-deps-1.0.0.tgz", {}, "sha512-v4w12JRjUGvfHDUP8vFDwu0gUWu04j0cv9hLb1Abf9VdaXu4XcrddYFTMVBVvmldKViGWH7jrb6xPJRF0wq6gw=="],
|
||||
|
||||
"pkg1": ["pkg1@workspace:packages/pkg1"],
|
||||
}
|
||||
}
|
||||
"
|
||||
`;
|
||||
221
test/cli/install/catalogs.test.ts
Normal file
221
test/cli/install/catalogs.test.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import { file, spawn, write } from "bun";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { exists } from "fs/promises";
|
||||
import { VerdaccioRegistry, bunEnv, bunExe, runBunInstall, stderrForInstall } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
var registry = new VerdaccioRegistry();
|
||||
|
||||
beforeAll(async () => {
|
||||
await registry.start();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
registry.stop();
|
||||
});
|
||||
|
||||
describe("basic", () => {
|
||||
async function createBasicCatalogMonorepo(packageDir: string, name: string) {
|
||||
const packageJson = {
|
||||
name,
|
||||
workspaces: {
|
||||
packages: ["packages/*"],
|
||||
catalog: {
|
||||
"no-deps": "2.0.0",
|
||||
},
|
||||
catalogs: {
|
||||
a: {
|
||||
"a-dep": "1.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
write(join(packageDir, "package.json"), JSON.stringify(packageJson)),
|
||||
write(
|
||||
join(packageDir, "packages", "pkg1", "package.json"),
|
||||
JSON.stringify({
|
||||
name: "pkg1",
|
||||
dependencies: {
|
||||
"no-deps": "catalog:",
|
||||
"a-dep": "catalog:a",
|
||||
},
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
return packageJson;
|
||||
}
|
||||
test("both catalog and catalogs", async () => {
|
||||
const { packageDir } = await registry.createTestDir();
|
||||
|
||||
await createBasicCatalogMonorepo(packageDir, "catalog-basic-1");
|
||||
|
||||
await runBunInstall(bunEnv, packageDir);
|
||||
|
||||
expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({
|
||||
name: "no-deps",
|
||||
version: "2.0.0",
|
||||
});
|
||||
|
||||
expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toEqual({
|
||||
name: "a-dep",
|
||||
version: "1.0.1",
|
||||
});
|
||||
|
||||
// another install does not save the lockfile
|
||||
await runBunInstall(bunEnv, packageDir, { savesLockfile: false });
|
||||
});
|
||||
|
||||
for (const binaryLockfile of [true, false]) {
|
||||
test(`detect changes (${binaryLockfile ? "bun.lockb" : "bun.lock"})`, async () => {
|
||||
const { packageDir } = await registry.createTestDir({ saveTextLockfile: !binaryLockfile });
|
||||
const packageJson = await createBasicCatalogMonorepo(packageDir, "catalog-basic-2");
|
||||
let { err } = await runBunInstall(bunEnv, packageDir);
|
||||
expect(err).toContain("Saved lockfile");
|
||||
|
||||
const initialLockfile = !binaryLockfile
|
||||
? (await file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234")
|
||||
: undefined;
|
||||
|
||||
if (!binaryLockfile) {
|
||||
expect(initialLockfile).toMatchSnapshot();
|
||||
} else {
|
||||
expect(await exists(join(packageDir, "bun.lockb"))).toBeTrue();
|
||||
}
|
||||
|
||||
expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({
|
||||
name: "no-deps",
|
||||
version: "2.0.0",
|
||||
});
|
||||
expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toEqual({
|
||||
name: "a-dep",
|
||||
version: "1.0.1",
|
||||
});
|
||||
|
||||
// update catalog
|
||||
packageJson.workspaces.catalog["no-deps"] = "1.0.0";
|
||||
await write(join(packageDir, "package.json"), JSON.stringify(packageJson));
|
||||
({ err } = await runBunInstall(bunEnv, packageDir, { savesLockfile: true }));
|
||||
expect(err).toContain("Saved lockfile");
|
||||
|
||||
if (!binaryLockfile) {
|
||||
const newLockfile = (await file(join(packageDir, "bun.lock")).text()).replaceAll(
|
||||
/localhost:\d+/g,
|
||||
"localhost:1234",
|
||||
);
|
||||
|
||||
expect(newLockfile).not.toEqual(initialLockfile);
|
||||
expect(newLockfile).toMatchSnapshot();
|
||||
} else {
|
||||
expect(await exists(join(packageDir, "bun.lockb"))).toBeTrue();
|
||||
}
|
||||
|
||||
expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({
|
||||
name: "no-deps",
|
||||
version: "1.0.0",
|
||||
});
|
||||
expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toEqual({
|
||||
name: "a-dep",
|
||||
version: "1.0.1",
|
||||
});
|
||||
|
||||
// update catalogs
|
||||
packageJson.workspaces.catalogs.a["a-dep"] = "1.0.10";
|
||||
await write(join(packageDir, "package.json"), JSON.stringify(packageJson));
|
||||
({ err } = await runBunInstall(bunEnv, packageDir, { savesLockfile: true }));
|
||||
expect(err).toContain("Saved lockfile");
|
||||
|
||||
if (!binaryLockfile) {
|
||||
const newLockfile = (await file(join(packageDir, "bun.lock")).text()).replaceAll(
|
||||
/localhost:\d+/g,
|
||||
"localhost:1234",
|
||||
);
|
||||
|
||||
expect(newLockfile).not.toEqual(initialLockfile);
|
||||
expect(newLockfile).toMatchSnapshot();
|
||||
} else {
|
||||
expect(await exists(join(packageDir, "bun.lockb"))).toBeTrue();
|
||||
}
|
||||
|
||||
expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({
|
||||
name: "no-deps",
|
||||
version: "1.0.0",
|
||||
});
|
||||
expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toEqual({
|
||||
name: "a-dep",
|
||||
version: "1.0.10",
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("errors", () => {
|
||||
test("fails gracefully when no catalog is found for a package", async () => {
|
||||
const { packageDir, packageJson } = await registry.createTestDir();
|
||||
|
||||
await write(
|
||||
packageJson,
|
||||
JSON.stringify({
|
||||
name: "catalog-error-1",
|
||||
workspaces: {
|
||||
// empty, any catalog should fail to resolve
|
||||
catalog: {},
|
||||
catalogs: {},
|
||||
},
|
||||
dependencies: {
|
||||
"no-deps": "catalog:",
|
||||
|
||||
// longer than 8
|
||||
"a-dep": "catalog:aaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const { stdout, stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: packageDir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: bunEnv,
|
||||
});
|
||||
|
||||
const out = await Bun.readableStreamToText(stdout);
|
||||
const err = stderrForInstall(await Bun.readableStreamToText(stderr));
|
||||
|
||||
expect(err).toContain("no-deps@catalog: failed to resolve");
|
||||
expect(err).toContain("a-dep@catalog:aaaaaaaaaaaaaaaaa failed to resolve");
|
||||
});
|
||||
|
||||
test("invalid dependency version", async () => {
|
||||
const { packageDir, packageJson } = await registry.createTestDir();
|
||||
await write(
|
||||
packageJson,
|
||||
JSON.stringify({
|
||||
name: "catalog-error-2",
|
||||
workspaces: {
|
||||
catalog: {
|
||||
"no-deps": ".:",
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
"no-deps": "catalog:",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const { stdout, stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: packageDir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: bunEnv,
|
||||
});
|
||||
|
||||
const out = await Bun.readableStreamToText(stdout);
|
||||
const err = stderrForInstall(await Bun.readableStreamToText(stderr));
|
||||
|
||||
expect(err).toContain("no-deps@catalog: failed to resolve");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user