Skip to content

Commit fdca594

Browse files
authored
feat: add --package-json flag to deno add/install/remove/uninstall (#33199)
- Adds a `--package-json` flag to `deno add`, `deno install`, `deno remove`, and `deno uninstall` subcommands - When set, forces all dependency management to use `package.json` instead of `deno.json`, bypassing the distance-based config file heuristic - Creates a new `package.json` if one doesn't exist - JSR packages are converted to their npm-compatible form (`npm:@jsr/...`) when written to `package.json`
1 parent 0b92830 commit fdca594

13 files changed

Lines changed: 175 additions & 7 deletions

File tree

cli/args/flags.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ pub struct AddFlags {
121121
pub default_registry: Option<DefaultRegistry>,
122122
pub lockfile_only: bool,
123123
pub save_exact: bool,
124+
pub package_json: bool,
124125
}
125126

126127
#[derive(Clone, Debug, Default, Eq, PartialEq)]
@@ -145,6 +146,7 @@ pub struct AuditFlags {
145146
pub struct RemoveFlags {
146147
pub packages: Vec<String>,
147148
pub lockfile_only: bool,
149+
pub package_json: bool,
148150
}
149151

150152
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -2350,6 +2352,7 @@ Or multiple dependencies at once:
23502352
.help("Save exact version without the caret (^)")
23512353
.action(ArgAction::SetTrue),
23522354
)
2355+
.arg(package_json_arg())
23532356
})
23542357
}
23552358

@@ -2520,6 +2523,7 @@ You can remove multiple dependencies at once:
25202523
)
25212524
.args(lock_args())
25222525
.arg(lockfile_only_arg())
2526+
.arg(package_json_arg())
25232527
})
25242528
}
25252529

@@ -3717,6 +3721,7 @@ These must be added to the path manually if required."), UnstableArgsConfig::Res
37173721
.conflicts_with("global"),
37183722
)
37193723
.arg(lockfile_only_arg().conflicts_with("global"))
3724+
.arg(package_json_arg().conflicts_with("entrypoint").conflicts_with("global"))
37203725
.arg(
37213726
Arg::new("os")
37223727
.long("os")
@@ -3758,6 +3763,15 @@ fn lockfile_only_arg() -> Arg {
37583763
.help("Install only updating the lockfile")
37593764
}
37603765

3766+
fn package_json_arg() -> Arg {
3767+
Arg::new("package-json")
3768+
.long("package-json")
3769+
.action(ArgAction::SetTrue)
3770+
.help(
3771+
"Force using package.json for dependency management instead of deno.json",
3772+
)
3773+
}
3774+
37613775
fn json_reference_subcommand() -> Command {
37623776
Command::new("json_reference").hide(true)
37633777
}
@@ -3975,6 +3989,7 @@ The installation root is determined, in order of precedence:
39753989
)
39763990
.args(lock_args())
39773991
.arg(lockfile_only_arg())
3992+
.arg(package_json_arg().conflicts_with("global"))
39783993
})
39793994
}
39803995

@@ -6427,6 +6442,7 @@ fn add_parse_inner(
64276442
default_registry,
64286443
lockfile_only: matches.get_flag("lockfile-only"),
64296444
save_exact: matches.get_flag("save-exact"),
6445+
package_json: matches.get_flag("package-json"),
64306446
}
64316447
}
64326448

@@ -6435,6 +6451,7 @@ fn remove_parse(flags: &mut Flags, matches: &mut ArgMatches) {
64356451
flags.subcommand = DenoSubcommand::Remove(RemoveFlags {
64366452
packages: matches.remove_many::<String>("packages").unwrap().collect(),
64376453
lockfile_only: matches.get_flag("lockfile-only"),
6454+
package_json: matches.get_flag("package-json"),
64386455
});
64396456
}
64406457

@@ -7345,6 +7362,7 @@ fn uninstall_parse(flags: &mut Flags, matches: &mut ArgMatches) {
73457362
UninstallKind::Local(RemoveFlags {
73467363
packages,
73477364
lockfile_only: matches.get_flag("lockfile-only"),
7365+
package_json: matches.get_flag("package-json"),
73487366
})
73497367
};
73507368

@@ -11473,6 +11491,7 @@ mod tests {
1147311491
kind: UninstallKind::Local(RemoveFlags {
1147411492
packages: vec!["@std/load".to_string()],
1147511493
lockfile_only: true,
11494+
package_json: false,
1147611495
}),
1147711496
}),
1147811497
frozen_lockfile: Some(true),
@@ -11489,6 +11508,7 @@ mod tests {
1148911508
kind: UninstallKind::Local(RemoveFlags {
1149011509
packages: vec!["file_server".to_string(), "@std/load".to_string()],
1149111510
lockfile_only: false,
11511+
package_json: false,
1149211512
}),
1149311513
}),
1149411514
..Flags::default()
@@ -14596,6 +14616,7 @@ mod tests {
1459614616
default_registry: Some(DefaultRegistry::Npm),
1459714617
lockfile_only: false,
1459814618
save_exact: false,
14619+
package_json: false,
1459914620
})
1460014621
);
1460114622
}
@@ -14614,6 +14635,7 @@ mod tests {
1461414635
default_registry: Some(DefaultRegistry::Npm),
1461514636
lockfile_only: true,
1461614637
save_exact: false,
14638+
package_json: false,
1461714639
});
1461814640
expected_flags.frozen_lockfile = Some(true);
1461914641
assert_eq!(r.unwrap(), expected_flags);
@@ -14628,6 +14650,7 @@ mod tests {
1462814650
default_registry: Some(DefaultRegistry::Npm),
1462914651
lockfile_only: false,
1463014652
save_exact: false,
14653+
package_json: false,
1463114654
}),
1463214655
);
1463314656
}
@@ -14641,6 +14664,7 @@ mod tests {
1464114664
default_registry: Some(DefaultRegistry::Npm),
1464214665
lockfile_only: false,
1464314666
save_exact: false,
14667+
package_json: false,
1464414668
}),
1464514669
);
1464614670
}
@@ -14654,6 +14678,7 @@ mod tests {
1465414678
default_registry: Some(DefaultRegistry::Jsr),
1465514679
lockfile_only: false,
1465614680
save_exact: false,
14681+
package_json: false,
1465714682
}),
1465814683
);
1465914684
}
@@ -14672,6 +14697,7 @@ mod tests {
1467214697
subcommand: DenoSubcommand::Remove(RemoveFlags {
1467314698
packages: svec!["@david/which"],
1467414699
lockfile_only: false,
14700+
package_json: false,
1467514701
}),
1467614702
..Flags::default()
1467714703
}
@@ -14691,6 +14717,7 @@ mod tests {
1469114717
subcommand: DenoSubcommand::Remove(RemoveFlags {
1469214718
packages: svec!["@david/which", "@luca/hello"],
1469314719
lockfile_only: true,
14720+
package_json: false,
1469414721
}),
1469514722
frozen_lockfile: Some(true),
1469614723
..Flags::default()

cli/tools/pm/mod.rs

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -345,14 +345,46 @@ impl std::fmt::Display for AddCommandName {
345345
}
346346
}
347347

348+
fn create_package_json(
349+
flags: &Arc<Flags>,
350+
options: &CliOptions,
351+
) -> Result<CliFactory, AnyError> {
352+
std::fs::write(options.initial_cwd().join("package.json"), "{}\n")
353+
.context("Failed to create package.json file")?;
354+
log::info!("Created package.json configuration file.");
355+
let factory = CliFactory::from_flags(flags.clone());
356+
Ok(factory)
357+
}
358+
348359
fn load_configs(
349360
flags: &Arc<Flags>,
350361
has_jsr_specifiers: impl FnOnce() -> bool,
362+
force_package_json: bool,
351363
) -> Result<(CliFactory, Option<ConfigUpdater>, Option<ConfigUpdater>), AnyError>
352364
{
353365
let cli_factory = CliFactory::from_flags(flags.clone());
354366
let options = cli_factory.cli_options()?;
355367
let start_dir = &options.start_dir;
368+
369+
if force_package_json {
370+
let npm_config = match start_dir.member_pkg_json() {
371+
Some(pkg_json) => Some(ConfigUpdater::new(
372+
ConfigKind::PackageJson,
373+
pkg_json.path.clone(),
374+
)?),
375+
None => {
376+
let pkg_json_path = options.initial_cwd().join("package.json");
377+
let factory = create_package_json(flags, options)?;
378+
return Ok((
379+
factory,
380+
Some(ConfigUpdater::new(ConfigKind::PackageJson, pkg_json_path)?),
381+
None,
382+
));
383+
}
384+
};
385+
return Ok((cli_factory, npm_config, None));
386+
}
387+
356388
let npm_config = match start_dir.member_pkg_json() {
357389
Some(pkg_json) => Some(ConfigUpdater::new(
358390
ConfigKind::PackageJson,
@@ -407,10 +439,12 @@ pub async fn add(
407439
cmd_name: AddCommandName,
408440
) -> Result<(), AnyError> {
409441
let save_exact = add_flags.save_exact;
410-
let (cli_factory, mut npm_config, mut deno_config) =
411-
load_configs(&flags, || {
412-
add_flags.packages.iter().any(|s| s.starts_with("jsr:"))
413-
})?;
442+
let force_package_json = add_flags.package_json;
443+
let (cli_factory, mut npm_config, mut deno_config) = load_configs(
444+
&flags,
445+
|| add_flags.packages.iter().any(|s| s.starts_with("jsr:")),
446+
force_package_json,
447+
)?;
414448

415449
if let Some(deno) = &deno_config
416450
&& deno.obj().get("importMap").is_some()
@@ -586,7 +620,11 @@ pub async fn add(
586620
selected_package.selected_version
587621
);
588622

589-
if selected_package.package_name.starts_with("npm:") && prefer_npm_config {
623+
if force_package_json {
624+
npm_config.as_mut().unwrap().add(selected_package, dev);
625+
} else if selected_package.package_name.starts_with("npm:")
626+
&& prefer_npm_config
627+
{
590628
if let Some(npm) = &mut npm_config {
591629
npm.add(selected_package, dev);
592630
} else {
@@ -901,9 +939,15 @@ pub async fn remove(
901939
flags: Arc<Flags>,
902940
remove_flags: RemoveFlags,
903941
) -> Result<(), AnyError> {
904-
let (_, npm_config, deno_config) = load_configs(&flags, || false)?;
942+
let force_package_json = remove_flags.package_json;
943+
let (_, npm_config, deno_config) =
944+
load_configs(&flags, || false, force_package_json)?;
905945

906-
let mut configs = [npm_config, deno_config];
946+
let mut configs = if force_package_json {
947+
[npm_config, None]
948+
} else {
949+
[npm_config, deno_config]
950+
};
907951

908952
let mut removed_packages = vec![];
909953

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"tempDir": true,
3+
"tests": {
4+
"forces_all_deps_to_package_json": {
5+
// When both deno.json and package.json exist, --package-json forces
6+
// all packages (including jsr) into package.json
7+
"steps": [
8+
{
9+
"args": "add --package-json npm:@denotest/esm-basic jsr:@denotest/add",
10+
"output": "add.out"
11+
},
12+
{
13+
"args": [
14+
"eval",
15+
"console.log(Deno.readTextFileSync('package.json').trim())"
16+
],
17+
"output": "package.json.out"
18+
},
19+
{
20+
"args": [
21+
"eval",
22+
"console.log(Deno.readTextFileSync('deno.json').trim())"
23+
],
24+
"output": "{}\n"
25+
}
26+
]
27+
},
28+
"creates_package_json_if_missing": {
29+
// When only deno.json exists, --package-json creates package.json
30+
"steps": [
31+
{
32+
"args": [
33+
"eval",
34+
"Deno.removeSync('package.json')"
35+
],
36+
"output": ""
37+
},
38+
{
39+
"args": "add --package-json npm:@denotest/esm-basic",
40+
"output": "add_create.out"
41+
},
42+
{
43+
"args": [
44+
"eval",
45+
"console.log(Deno.readTextFileSync('package.json').trim())"
46+
],
47+
"output": "created_package.json.out"
48+
}
49+
]
50+
}
51+
}
52+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[UNORDERED_START]
2+
Add npm:@denotest/esm-basic@1.0.0
3+
Add jsr:@denotest/add@1.0.0
4+
[UNORDERED_END]
5+
[WILDCARD]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Created package.json configuration file.
2+
[WILDCARD]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dependencies": {
3+
"@denotest/esm-basic": "^1.0.0"
4+
}
5+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"dependencies": {
3+
"@denotest/add": "npm:@jsr/denotest__add@^1.0.0",
4+
"@denotest/esm-basic": "^1.0.0"
5+
}
6+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"tempDir": true,
3+
"steps": [
4+
{
5+
// First add a package to package.json using --package-json
6+
"args": "add --package-json npm:@denotest/esm-basic",
7+
"output": "[WILDCARD]"
8+
},
9+
{
10+
// Then remove it using --package-json
11+
"args": "remove --package-json @denotest/esm-basic",
12+
"output": "remove.out"
13+
},
14+
{
15+
"args": [
16+
"eval",
17+
"console.log(Deno.readTextFileSync('package.json').trim())"
18+
],
19+
"output": "{\n}\n"
20+
}
21+
]
22+
}

0 commit comments

Comments
 (0)