Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions completions/bun-cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -2269,14 +2269,14 @@
},
{
"name": "no-save",
"description": "Don't update package.json or save a lockfile",
"description": "Don't update package.json or save a lockfile (the default)",
"hasValue": false,
"required": false,
"multiple": false
},
{
"name": "save",
"description": "Save to package.json (true by default)",
"description": "Update package.json and save a lockfile (false by default)",
"hasValue": false,
"required": false,
"multiple": false
Expand Down Expand Up @@ -2528,14 +2528,14 @@
},
{
"name": "no-save",
"description": "Don't update package.json or save a lockfile",
"description": "Don't update package.json or save a lockfile (the default)",
"hasValue": false,
"required": false,
"multiple": false
},
{
"name": "save",
"description": "Save to package.json (true by default)",
"description": "Update package.json and save a lockfile (false by default)",
"hasValue": false,
"required": false,
"multiple": false
Expand Down
6 changes: 3 additions & 3 deletions docs/snippets/cli/link.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ bun link <packages>
</ParamField>

<ParamField path="--no-save" type="boolean">
Don't update <code>package.json</code> or save a lockfile
Don't update <code>package.json</code> or save a lockfile (the default)
</ParamField>

<ParamField path="--save" type="boolean" default="true">
Save to <code>package.json</code>
<ParamField path="--save" type="boolean">
Update <code>package.json</code> and save a lockfile (false by default)
</ParamField>

<ParamField path="--trust" type="boolean">
Expand Down
39 changes: 32 additions & 7 deletions src/install/PackageManager/CommandLineArguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,24 @@ const PRODUCTION_PARAMS: &[ParamType] = &[
clap::param!("-P, --prod"),
];

const SHARED_TAIL_PARAMS: &[ParamType] = &[
const SAVE_PARAMS: &[ParamType] = &[
clap::param!(
"--no-save Don't update package.json or save a lockfile"
),
clap::param!("--save Save to package.json (true by default)"),
];

// `parse` gives link and unlink the opposite default: they only save when --save is passed.
const LINK_SAVE_PARAMS: &[ParamType] = &[
clap::param!(
"--no-save Don't update package.json or save a lockfile (the default)"
),
clap::param!(
"--save Update package.json and save a lockfile (false by default)"
),
];

const SHARED_TAIL_PARAMS: &[ParamType] = &[
clap::param!(
"--ca <STR>... Provide a Certificate Authority signing certificate"
),
Expand Down Expand Up @@ -128,8 +141,19 @@ const SHARED_TAIL_PARAMS: &[ParamType] = &[
clap::param!("-h, --help Print this help menu"),
];

const SHARED_PARAMS: &[ParamType] =
concat_params![SHARED_HEAD_PARAMS, PRODUCTION_PARAMS, SHARED_TAIL_PARAMS];
const SHARED_PARAMS: &[ParamType] = concat_params![
SHARED_HEAD_PARAMS,
PRODUCTION_PARAMS,
SAVE_PARAMS,
SHARED_TAIL_PARAMS
];

const LINK_SHARED_PARAMS: &[ParamType] = concat_params![
SHARED_HEAD_PARAMS,
PRODUCTION_PARAMS,
LINK_SAVE_PARAMS,
SHARED_TAIL_PARAMS
];

pub(crate) static INSTALL_PARAMS: &[ParamType] = concat_params![
SHARED_PARAMS,
Expand Down Expand Up @@ -165,6 +189,7 @@ pub(crate) static UPDATE_PARAMS: &[ParamType] = concat_params![
),
clap::param!("-P, --prod"),
],
SAVE_PARAMS,
SHARED_TAIL_PARAMS,
&[
clap::param!(
Expand Down Expand Up @@ -269,14 +294,14 @@ pub(crate) static REMOVE_PARAMS: &[ParamType] = concat_params![
];

pub(crate) static LINK_PARAMS: &[ParamType] = concat_params![
SHARED_PARAMS,
LINK_SHARED_PARAMS,
&[clap::param!(
"<POS> ... \"name\" install package as a link"
),]
];

pub(crate) static UNLINK_PARAMS: &[ParamType] = concat_params![
SHARED_PARAMS,
LINK_SHARED_PARAMS,
&[clap::param!(
"<POS> ... \"name\" uninstall package as a link"
),]
Expand Down Expand Up @@ -1420,8 +1445,8 @@ Full documentation is available at <magenta>https://bun.com/docs/pm/cli/prune<r>
cli.tolerate_republish = args.flag(b"--tolerate-republish");
}

// link and unlink default to not saving, all others default to
// saving.
// link and unlink default to not saving (their --help says so via
// `LINK_SAVE_PARAMS`), all others default to saving.
Comment thread
robobun marked this conversation as resolved.
Outdated
if matches!(subcommand, Subcommand::Link | Subcommand::Unlink) {
cli.no_save = !args.flag(b"--save");
} else {
Expand Down
96 changes: 96 additions & 0 deletions test/cli/install/bun-link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
isWindows,
readdirSorted,
runBunInstall,
tempDir,
tmpdirSync,
toBeValidBin,
toHaveBins,
Expand Down Expand Up @@ -471,3 +472,98 @@ it("should link dependency without crashing", async () => {
// This should fail with a non-zero exit code.
expect(await exited4).toBe(1);
});

// The install family shares one flag table, but `bun link <package>` and `bun unlink` invert the
// --save default (CommandLineArguments.parse): they only touch package.json and the lockfile when
// --save is passed, so their --help has to describe the flag differently from bun install's.
it.each([
["install", "Save to package.json (true by default)", "Don't update package.json or save a lockfile"],
["add", "Save to package.json (true by default)", "Don't update package.json or save a lockfile"],
["update", "Save to package.json (true by default)", "Don't update package.json or save a lockfile"],
["remove", "Save to package.json (true by default)", "Don't update package.json or save a lockfile"],
[
"link",
"Update package.json and save a lockfile (false by default)",
"Don't update package.json or save a lockfile (the default)",
],
[
"unlink",
"Update package.json and save a lockfile (false by default)",
"Don't update package.json or save a lockfile (the default)",
],
])("bun %s --help describes the --save default it actually has", async (subcommand, save, noSave) => {
await using proc = spawn({
cmd: [bunExe(), subcommand, "--help"],
env,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

const lines = stdout.split(/\r?\n/).map(line => line.trim());
const descriptionOf = (flag: string) =>
lines
.find(line => line.startsWith(`${flag} `))
?.slice(flag.length)
.trim();
expect({ "--save": descriptionOf("--save"), "--no-save": descriptionOf("--no-save"), stderr, exitCode }).toEqual({
"--save": save,
"--no-save": noSave,
stderr: "",
exitCode: 0,
});
});

it("bun link <package> only writes package.json and a lockfile when --save is passed", async () => {
const appPackageJson = JSON.stringify({ name: "app", version: "1.0.0" });
using dir = tempDir("bun-link-save", {
"lib/package.json": JSON.stringify({ name: "lib-to-link", version: "1.0.0" }),
"app/package.json": appPackageJson,
});
const app = join(String(dir), "app");
// Register the package in a global dir private to this test instead of the machine's real one.
const linkEnv = {
...env,
BUN_INSTALL_GLOBAL_DIR: join(String(dir), "global", "install", "global"),
BUN_INSTALL_BIN: join(String(dir), "global", "bin"),
};
async function bun(cwd: string, ...args: string[]) {
await using proc = spawn({
cmd: [bunExe(), ...args],
cwd,
env: linkEnv,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return { stdout, stderr, exitCode };
}

let { stdout, stderr, exitCode } = await bun(join(String(dir), "lib"), "link");
expect(stderr).toBe("");
expect(stdout).toContain('Success! Registered "lib-to-link"');
expect(exitCode).toBe(0);

({ stdout, stderr, exitCode } = await bun(app, "link", "lib-to-link"));
expect(stderr).toBe("");
expect(stdout).toContain("installed lib-to-link@link:lib-to-link");
expect(exitCode).toBe(0);
expect(await file(join(app, "node_modules", "lib-to-link", "package.json")).json()).toEqual({
name: "lib-to-link",
version: "1.0.0",
});
expect(await file(join(app, "package.json")).text()).toBe(appPackageJson);
expect(await readdirSorted(app)).toEqual(["node_modules", "package.json"]);

({ stdout, stderr, exitCode } = await bun(app, "link", "lib-to-link", "--save"));
expect(stderr).toContain("Saved lockfile");
expect(stdout).toContain("installed lib-to-link@link:lib-to-link");
expect(exitCode).toBe(0);
expect(await file(join(app, "package.json")).json()).toEqual({
name: "app",
version: "1.0.0",
dependencies: { "lib-to-link": "link:lib-to-link" },
});
expect(await readdirSorted(app)).toEqual(["bun.lock", "node_modules", "package.json"]);
});
Loading