mirror of
https://github.com/denoland/deno.git
synced 2024-11-24 15:19:26 -05:00
feat(add): Add npm packages to package.json if present (#25477)
Closes https://github.com/denoland/deno/issues/25321 Ended up being a larger refactoring, since we're now juggling (potentially) two config files in the same `add`, instead of choosing one. I don't love the shape of the code, but I think it's good enough Some smaller side improvements: - `deno remove` supports `jsonc` - `deno install --dev` will be a really simple change - if `deno remove` removes the last import/dependency in the `imports`/`dependencies`/`devDependencies` field, it removes the field instead of leaving an empty object
This commit is contained in:
parent
f0a3d20642
commit
51f5f5789b
19 changed files with 531 additions and 231 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1253,6 +1253,7 @@ dependencies = [
|
||||||
"which 4.4.2",
|
"which 4.4.2",
|
||||||
"winapi",
|
"winapi",
|
||||||
"winres",
|
"winres",
|
||||||
|
"yoke",
|
||||||
"zeromq",
|
"zeromq",
|
||||||
"zip",
|
"zip",
|
||||||
"zstd",
|
"zstd",
|
||||||
|
|
|
@ -193,6 +193,7 @@ url = { version = "< 2.5.0", features = ["serde", "expose_internals"] }
|
||||||
uuid = { version = "1.3.0", features = ["v4"] }
|
uuid = { version = "1.3.0", features = ["v4"] }
|
||||||
webpki-roots = "0.26"
|
webpki-roots = "0.26"
|
||||||
which = "4.2.5"
|
which = "4.2.5"
|
||||||
|
yoke = { version = "0.7.4", features = ["derive"] }
|
||||||
zeromq = { version = "=0.4.0", default-features = false, features = ["tcp-transport", "tokio-runtime"] }
|
zeromq = { version = "=0.4.0", default-features = false, features = ["tcp-transport", "tokio-runtime"] }
|
||||||
zstd = "=0.12.4"
|
zstd = "=0.12.4"
|
||||||
|
|
||||||
|
|
|
@ -161,6 +161,7 @@ typed-arena = "=2.0.2"
|
||||||
uuid = { workspace = true, features = ["serde"] }
|
uuid = { workspace = true, features = ["serde"] }
|
||||||
walkdir = "=2.3.2"
|
walkdir = "=2.3.2"
|
||||||
which.workspace = true
|
which.workspace = true
|
||||||
|
yoke.workspace = true
|
||||||
zeromq.workspace = true
|
zeromq.workspace = true
|
||||||
zip = { version = "2.1.6", default-features = false, features = ["deflate-flate2"] }
|
zip = { version = "2.1.6", default-features = false, features = ["deflate-flate2"] }
|
||||||
zstd.workspace = true
|
zstd.workspace = true
|
||||||
|
|
|
@ -7,7 +7,6 @@ use deno_semver::jsr::JsrPackageReqReference;
|
||||||
use deno_semver::npm::NpmPackageReqReference;
|
use deno_semver::npm::NpmPackageReqReference;
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::path::Path;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
@ -26,9 +25,11 @@ use deno_semver::package::PackageReq;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use jsonc_parser::ast::ObjectProp;
|
use jsonc_parser::ast::ObjectProp;
|
||||||
use jsonc_parser::ast::Value;
|
use jsonc_parser::ast::Value;
|
||||||
|
use yoke::Yoke;
|
||||||
|
|
||||||
use crate::args::AddFlags;
|
use crate::args::AddFlags;
|
||||||
use crate::args::CacheSetting;
|
use crate::args::CacheSetting;
|
||||||
|
use crate::args::CliOptions;
|
||||||
use crate::args::Flags;
|
use crate::args::Flags;
|
||||||
use crate::args::RemoveFlags;
|
use crate::args::RemoveFlags;
|
||||||
use crate::factory::CliFactory;
|
use crate::factory::CliFactory;
|
||||||
|
@ -56,115 +57,303 @@ impl DenoConfigFormat {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct DenoConfig {
|
||||||
|
config: Arc<deno_config::deno_json::ConfigFile>,
|
||||||
|
format: DenoConfigFormat,
|
||||||
|
imports: IndexMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deno_json_imports(
|
||||||
|
config: &deno_config::deno_json::ConfigFile,
|
||||||
|
) -> Result<IndexMap<String, String>, AnyError> {
|
||||||
|
Ok(
|
||||||
|
config
|
||||||
|
.json
|
||||||
|
.imports
|
||||||
|
.clone()
|
||||||
|
.map(|imports| {
|
||||||
|
serde_json::from_value(imports)
|
||||||
|
.map_err(|err| anyhow!("Malformed \"imports\" configuration: {err}"))
|
||||||
|
})
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
impl DenoConfig {
|
||||||
|
fn from_options(options: &CliOptions) -> Result<Option<Self>, AnyError> {
|
||||||
|
let start_dir = &options.start_dir;
|
||||||
|
if let Some(config) = start_dir.maybe_deno_json() {
|
||||||
|
Ok(Some(Self {
|
||||||
|
imports: deno_json_imports(config)?,
|
||||||
|
config: config.clone(),
|
||||||
|
format: DenoConfigFormat::from_specifier(&config.specifier)?,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add(&mut self, selected: SelectedPackage) {
|
||||||
|
self.imports.insert(
|
||||||
|
selected.import_name,
|
||||||
|
format!("{}@{}", selected.package_name, selected.version_req),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, package: &str) -> bool {
|
||||||
|
self.imports.shift_remove(package).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_import_fields(
|
||||||
|
&mut self,
|
||||||
|
) -> Vec<(&'static str, IndexMap<String, String>)> {
|
||||||
|
vec![("imports", std::mem::take(&mut self.imports))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NpmConfig {
|
||||||
|
fn from_options(options: &CliOptions) -> Result<Option<Self>, AnyError> {
|
||||||
|
let start_dir = &options.start_dir;
|
||||||
|
if let Some(pkg_json) = start_dir.maybe_pkg_json() {
|
||||||
|
Ok(Some(Self {
|
||||||
|
dependencies: pkg_json.dependencies.clone().unwrap_or_default(),
|
||||||
|
dev_dependencies: pkg_json.dev_dependencies.clone().unwrap_or_default(),
|
||||||
|
config: pkg_json.clone(),
|
||||||
|
fmt_options: None,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add(&mut self, selected: SelectedPackage, dev: bool) {
|
||||||
|
let (name, version) = package_json_dependency_entry(selected);
|
||||||
|
if dev {
|
||||||
|
self.dev_dependencies.insert(name, version);
|
||||||
|
} else {
|
||||||
|
self.dependencies.insert(name, version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, package: &str) -> bool {
|
||||||
|
let in_deps = self.dependencies.shift_remove(package).is_some();
|
||||||
|
let in_dev_deps = self.dev_dependencies.shift_remove(package).is_some();
|
||||||
|
in_deps || in_dev_deps
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_import_fields(
|
||||||
|
&mut self,
|
||||||
|
) -> Vec<(&'static str, IndexMap<String, String>)> {
|
||||||
|
vec![
|
||||||
|
("dependencies", std::mem::take(&mut self.dependencies)),
|
||||||
|
(
|
||||||
|
"devDependencies",
|
||||||
|
std::mem::take(&mut self.dev_dependencies),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NpmConfig {
|
||||||
|
config: Arc<deno_node::PackageJson>,
|
||||||
|
fmt_options: Option<FmtOptionsConfig>,
|
||||||
|
dependencies: IndexMap<String, String>,
|
||||||
|
dev_dependencies: IndexMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
enum DenoOrPackageJson {
|
enum DenoOrPackageJson {
|
||||||
Deno(Arc<deno_config::deno_json::ConfigFile>, DenoConfigFormat),
|
Deno(DenoConfig),
|
||||||
Npm(Arc<deno_node::PackageJson>, Option<FmtOptionsConfig>),
|
Npm(NpmConfig),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DenoConfig> for DenoOrPackageJson {
|
||||||
|
fn from(config: DenoConfig) -> Self {
|
||||||
|
Self::Deno(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NpmConfig> for DenoOrPackageJson {
|
||||||
|
fn from(config: NpmConfig) -> Self {
|
||||||
|
Self::Npm(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper around `jsonc_parser::ast::Object` that can be stored in a `Yoke`
|
||||||
|
#[derive(yoke::Yokeable)]
|
||||||
|
struct JsoncObjectView<'a>(jsonc_parser::ast::Object<'a>);
|
||||||
|
|
||||||
|
struct ConfigUpdater {
|
||||||
|
config: DenoOrPackageJson,
|
||||||
|
// the `Yoke` is so we can carry the parsed object (which borrows from
|
||||||
|
// the source) along with the source itself
|
||||||
|
ast: Yoke<JsoncObjectView<'static>, String>,
|
||||||
|
path: PathBuf,
|
||||||
|
modified: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigUpdater {
|
||||||
|
fn obj(&self) -> &jsonc_parser::ast::Object<'_> {
|
||||||
|
&self.ast.get().0
|
||||||
|
}
|
||||||
|
fn contents(&self) -> &str {
|
||||||
|
self.ast.backing_cart()
|
||||||
|
}
|
||||||
|
async fn maybe_new(
|
||||||
|
config: Option<impl Into<DenoOrPackageJson>>,
|
||||||
|
) -> Result<Option<Self>, AnyError> {
|
||||||
|
if let Some(config) = config {
|
||||||
|
Ok(Some(Self::new(config.into()).await?))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn new(config: DenoOrPackageJson) -> Result<Self, AnyError> {
|
||||||
|
let specifier = config.specifier();
|
||||||
|
if specifier.scheme() != "file" {
|
||||||
|
bail!("Can't update a remote configuration file");
|
||||||
|
}
|
||||||
|
let config_file_path = specifier.to_file_path().map_err(|_| {
|
||||||
|
anyhow!("Specifier {specifier:?} is an invalid file path")
|
||||||
|
})?;
|
||||||
|
let config_file_contents = {
|
||||||
|
let contents = tokio::fs::read_to_string(&config_file_path)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!("Reading config file at: {}", config_file_path.display())
|
||||||
|
})?;
|
||||||
|
if contents.trim().is_empty() {
|
||||||
|
"{}\n".into()
|
||||||
|
} else {
|
||||||
|
contents
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let ast = Yoke::try_attach_to_cart(config_file_contents, |contents| {
|
||||||
|
let ast = jsonc_parser::parse_to_ast(
|
||||||
|
contents,
|
||||||
|
&Default::default(),
|
||||||
|
&Default::default(),
|
||||||
|
)
|
||||||
|
.with_context(|| {
|
||||||
|
format!("Failed to parse config file at {}", specifier)
|
||||||
|
})?;
|
||||||
|
let obj = match ast.value {
|
||||||
|
Some(Value::Object(obj)) => obj,
|
||||||
|
_ => bail!(
|
||||||
|
"Failed to update config file at {}, expected an object",
|
||||||
|
specifier
|
||||||
|
),
|
||||||
|
};
|
||||||
|
Ok(JsoncObjectView(obj))
|
||||||
|
})?;
|
||||||
|
Ok(Self {
|
||||||
|
config,
|
||||||
|
ast,
|
||||||
|
path: config_file_path,
|
||||||
|
modified: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add(&mut self, selected: SelectedPackage, dev: bool) {
|
||||||
|
match &mut self.config {
|
||||||
|
DenoOrPackageJson::Deno(deno) => deno.add(selected),
|
||||||
|
DenoOrPackageJson::Npm(npm) => npm.add(selected, dev),
|
||||||
|
}
|
||||||
|
self.modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, package: &str) -> bool {
|
||||||
|
let removed = match &mut self.config {
|
||||||
|
DenoOrPackageJson::Deno(deno) => deno.remove(package),
|
||||||
|
DenoOrPackageJson::Npm(npm) => npm.remove(package),
|
||||||
|
};
|
||||||
|
if removed {
|
||||||
|
self.modified = true;
|
||||||
|
}
|
||||||
|
removed
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn commit(mut self) -> Result<(), AnyError> {
|
||||||
|
if !self.modified {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let import_fields = self.config.take_import_fields();
|
||||||
|
|
||||||
|
let fmt_config_options = self.config.fmt_options();
|
||||||
|
|
||||||
|
let new_text = update_config_file_content(
|
||||||
|
self.obj(),
|
||||||
|
self.contents(),
|
||||||
|
fmt_config_options,
|
||||||
|
import_fields.into_iter().map(|(k, v)| {
|
||||||
|
(
|
||||||
|
k,
|
||||||
|
if v.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(generate_imports(v.into_iter().collect()))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
self.config.file_name(),
|
||||||
|
);
|
||||||
|
|
||||||
|
tokio::fs::write(&self.path, new_text).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DenoOrPackageJson {
|
impl DenoOrPackageJson {
|
||||||
fn specifier(&self) -> Cow<ModuleSpecifier> {
|
fn specifier(&self) -> Cow<ModuleSpecifier> {
|
||||||
match self {
|
match self {
|
||||||
Self::Deno(d, ..) => Cow::Borrowed(&d.specifier),
|
Self::Deno(d, ..) => Cow::Borrowed(&d.config.specifier),
|
||||||
Self::Npm(n, ..) => Cow::Owned(n.specifier()),
|
Self::Npm(n, ..) => Cow::Owned(n.config.specifier()),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the existing imports/dependencies from the config.
|
|
||||||
fn existing_imports(&self) -> Result<IndexMap<String, String>, AnyError> {
|
|
||||||
match self {
|
|
||||||
DenoOrPackageJson::Deno(deno, ..) => {
|
|
||||||
if let Some(imports) = deno.json.imports.clone() {
|
|
||||||
match serde_json::from_value(imports) {
|
|
||||||
Ok(map) => Ok(map),
|
|
||||||
Err(err) => {
|
|
||||||
bail!("Malformed \"imports\" configuration: {err}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(Default::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DenoOrPackageJson::Npm(npm, ..) => {
|
|
||||||
Ok(npm.dependencies.clone().unwrap_or_default())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fmt_options(&self) -> FmtOptionsConfig {
|
fn fmt_options(&self) -> FmtOptionsConfig {
|
||||||
match self {
|
match self {
|
||||||
DenoOrPackageJson::Deno(deno, ..) => deno
|
DenoOrPackageJson::Deno(deno, ..) => deno
|
||||||
|
.config
|
||||||
.to_fmt_config()
|
.to_fmt_config()
|
||||||
.ok()
|
.ok()
|
||||||
.map(|f| f.options)
|
.map(|f| f.options)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
DenoOrPackageJson::Npm(_, config) => config.clone().unwrap_or_default(),
|
DenoOrPackageJson::Npm(config) => {
|
||||||
|
config.fmt_options.clone().unwrap_or_default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn imports_key(&self) -> &'static str {
|
fn take_import_fields(
|
||||||
|
&mut self,
|
||||||
|
) -> Vec<(&'static str, IndexMap<String, String>)> {
|
||||||
match self {
|
match self {
|
||||||
DenoOrPackageJson::Deno(..) => "imports",
|
Self::Deno(d) => d.take_import_fields(),
|
||||||
DenoOrPackageJson::Npm(..) => "dependencies",
|
Self::Npm(n) => n.take_import_fields(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file_name(&self) -> &'static str {
|
fn file_name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
DenoOrPackageJson::Deno(_, format) => match format {
|
DenoOrPackageJson::Deno(config) => match config.format {
|
||||||
DenoConfigFormat::Json => "deno.json",
|
DenoConfigFormat::Json => "deno.json",
|
||||||
DenoConfigFormat::Jsonc => "deno.jsonc",
|
DenoConfigFormat::Jsonc => "deno.jsonc",
|
||||||
},
|
},
|
||||||
DenoOrPackageJson::Npm(..) => "package.json",
|
DenoOrPackageJson::Npm(..) => "package.json",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn is_npm(&self) -> bool {
|
fn create_deno_json(
|
||||||
matches!(self, Self::Npm(..))
|
flags: &Arc<Flags>,
|
||||||
}
|
options: &CliOptions,
|
||||||
|
) -> Result<CliFactory, AnyError> {
|
||||||
/// Get the preferred config file to operate on
|
std::fs::write(options.initial_cwd().join("deno.json"), "{}\n")
|
||||||
/// given the flags. If no config file is present,
|
.context("Failed to create deno.json file")?;
|
||||||
/// creates a `deno.json` file - in this case
|
log::info!("Created deno.json configuration file.");
|
||||||
/// we also return a new `CliFactory` that knows about
|
let factory = CliFactory::from_flags(flags.clone());
|
||||||
/// the new config
|
Ok(factory)
|
||||||
fn from_flags(flags: Arc<Flags>) -> Result<(Self, CliFactory), AnyError> {
|
|
||||||
let factory = CliFactory::from_flags(flags.clone());
|
|
||||||
let options = factory.cli_options()?;
|
|
||||||
let start_dir = &options.start_dir;
|
|
||||||
|
|
||||||
match (start_dir.maybe_deno_json(), start_dir.maybe_pkg_json()) {
|
|
||||||
// when both are present, for now,
|
|
||||||
// default to deno.json
|
|
||||||
(Some(deno), Some(_) | None) => Ok((
|
|
||||||
DenoOrPackageJson::Deno(
|
|
||||||
deno.clone(),
|
|
||||||
DenoConfigFormat::from_specifier(&deno.specifier)?,
|
|
||||||
),
|
|
||||||
factory,
|
|
||||||
)),
|
|
||||||
(None, Some(package_json)) => {
|
|
||||||
Ok((DenoOrPackageJson::Npm(package_json.clone(), None), factory))
|
|
||||||
}
|
|
||||||
(None, None) => {
|
|
||||||
std::fs::write(options.initial_cwd().join("deno.json"), "{}\n")
|
|
||||||
.context("Failed to create deno.json file")?;
|
|
||||||
drop(factory); // drop to prevent use
|
|
||||||
log::info!("Created deno.json configuration file.");
|
|
||||||
let factory = CliFactory::from_flags(flags.clone());
|
|
||||||
let options = factory.cli_options()?.clone();
|
|
||||||
let start_dir = &options.start_dir;
|
|
||||||
Ok((
|
|
||||||
DenoOrPackageJson::Deno(
|
|
||||||
start_dir.maybe_deno_json().cloned().ok_or_else(|| {
|
|
||||||
anyhow!("config not found, but it was just created")
|
|
||||||
})?,
|
|
||||||
DenoConfigFormat::Json,
|
|
||||||
),
|
|
||||||
factory,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn package_json_dependency_entry(
|
fn package_json_dependency_entry(
|
||||||
|
@ -199,19 +388,53 @@ impl std::fmt::Display for AddCommandName {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_configs(
|
||||||
|
flags: &Arc<Flags>,
|
||||||
|
) -> Result<(CliFactory, Option<NpmConfig>, Option<DenoConfig>), AnyError> {
|
||||||
|
let cli_factory = CliFactory::from_flags(flags.clone());
|
||||||
|
let options = cli_factory.cli_options()?;
|
||||||
|
let npm_config = NpmConfig::from_options(options)?;
|
||||||
|
let (cli_factory, deno_config) = match DenoConfig::from_options(options)? {
|
||||||
|
Some(config) => (cli_factory, Some(config)),
|
||||||
|
None if npm_config.is_some() => (cli_factory, None),
|
||||||
|
None => {
|
||||||
|
let factory = create_deno_json(flags, options)?;
|
||||||
|
let options = factory.cli_options()?.clone();
|
||||||
|
(
|
||||||
|
factory,
|
||||||
|
Some(
|
||||||
|
DenoConfig::from_options(&options)?.expect("Just created deno.json"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert!(deno_config.is_some() || npm_config.is_some());
|
||||||
|
Ok((cli_factory, npm_config, deno_config))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn add(
|
pub async fn add(
|
||||||
flags: Arc<Flags>,
|
flags: Arc<Flags>,
|
||||||
add_flags: AddFlags,
|
add_flags: AddFlags,
|
||||||
cmd_name: AddCommandName,
|
cmd_name: AddCommandName,
|
||||||
) -> Result<(), AnyError> {
|
) -> Result<(), AnyError> {
|
||||||
let (config_file, cli_factory) =
|
let (cli_factory, npm_config, deno_config) = load_configs(&flags)?;
|
||||||
DenoOrPackageJson::from_flags(flags.clone())?;
|
let mut npm_config = ConfigUpdater::maybe_new(npm_config).await?;
|
||||||
|
let mut deno_config = ConfigUpdater::maybe_new(deno_config).await?;
|
||||||
|
|
||||||
let config_specifier = config_file.specifier();
|
if let Some(deno) = &deno_config {
|
||||||
if config_specifier.scheme() != "file" {
|
let specifier = deno.config.specifier();
|
||||||
bail!("Can't add dependencies to a remote configuration file");
|
if deno.obj().get_string("importMap").is_some() {
|
||||||
|
bail!(
|
||||||
|
concat!(
|
||||||
|
"`deno {}` is not supported when configuration file contains an \"importMap\" field. ",
|
||||||
|
"Inline the import map into the Deno configuration file.\n",
|
||||||
|
" at {}",
|
||||||
|
),
|
||||||
|
cmd_name,
|
||||||
|
specifier
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let config_file_path = config_specifier.to_file_path().unwrap();
|
|
||||||
|
|
||||||
let http_client = cli_factory.http_client_provider();
|
let http_client = cli_factory.http_client_provider();
|
||||||
|
|
||||||
|
@ -279,39 +502,6 @@ pub async fn add(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let config_file_contents = {
|
|
||||||
let contents = tokio::fs::read_to_string(&config_file_path).await.unwrap();
|
|
||||||
if contents.trim().is_empty() {
|
|
||||||
"{}\n".into()
|
|
||||||
} else {
|
|
||||||
contents
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let ast = jsonc_parser::parse_to_ast(
|
|
||||||
&config_file_contents,
|
|
||||||
&Default::default(),
|
|
||||||
&Default::default(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let obj = match ast.value {
|
|
||||||
Some(Value::Object(obj)) => obj,
|
|
||||||
_ => bail!("Failed updating config file due to no object."),
|
|
||||||
};
|
|
||||||
|
|
||||||
if obj.get_string("importMap").is_some() {
|
|
||||||
bail!(
|
|
||||||
concat!(
|
|
||||||
"`deno add` is not supported when configuration file contains an \"importMap\" field. ",
|
|
||||||
"Inline the import map into the Deno configuration file.\n",
|
|
||||||
" at {}",
|
|
||||||
),
|
|
||||||
config_specifier
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut existing_imports = config_file.existing_imports()?;
|
|
||||||
|
|
||||||
let is_npm = config_file.is_npm();
|
|
||||||
for selected_package in selected_packages {
|
for selected_package in selected_packages {
|
||||||
log::info!(
|
log::info!(
|
||||||
"Add {}{}{}",
|
"Add {}{}{}",
|
||||||
|
@ -320,39 +510,32 @@ pub async fn add(
|
||||||
selected_package.selected_version
|
selected_package.selected_version
|
||||||
);
|
);
|
||||||
|
|
||||||
if is_npm {
|
if selected_package.package_name.starts_with("npm:") {
|
||||||
let (name, version) = package_json_dependency_entry(selected_package);
|
if let Some(npm) = &mut npm_config {
|
||||||
existing_imports.insert(name, version)
|
npm.add(selected_package, false);
|
||||||
|
} else {
|
||||||
|
deno_config.as_mut().unwrap().add(selected_package, false);
|
||||||
|
}
|
||||||
|
} else if let Some(deno) = &mut deno_config {
|
||||||
|
deno.add(selected_package, false);
|
||||||
} else {
|
} else {
|
||||||
existing_imports.insert(
|
npm_config.as_mut().unwrap().add(selected_package, false);
|
||||||
selected_package.import_name,
|
}
|
||||||
format!(
|
|
||||||
"{}@{}",
|
|
||||||
selected_package.package_name, selected_package.version_req
|
|
||||||
),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
let mut import_list: Vec<(String, String)> =
|
|
||||||
existing_imports.into_iter().collect();
|
|
||||||
|
|
||||||
import_list.sort_by(|(k1, _), (k2, _)| k1.cmp(k2));
|
let mut commit_futures = vec![];
|
||||||
let generated_imports = generate_imports(import_list);
|
if let Some(npm) = npm_config {
|
||||||
|
commit_futures.push(npm.commit());
|
||||||
|
}
|
||||||
|
if let Some(deno) = deno_config {
|
||||||
|
commit_futures.push(deno.commit());
|
||||||
|
}
|
||||||
|
let commit_futures =
|
||||||
|
deno_core::futures::future::join_all(commit_futures).await;
|
||||||
|
|
||||||
let fmt_config_options = config_file.fmt_options();
|
for result in commit_futures {
|
||||||
|
result.context("Failed to update configuration file")?;
|
||||||
let new_text = update_config_file_content(
|
}
|
||||||
obj,
|
|
||||||
&config_file_contents,
|
|
||||||
generated_imports,
|
|
||||||
fmt_config_options,
|
|
||||||
config_file.imports_key(),
|
|
||||||
config_file.file_name(),
|
|
||||||
);
|
|
||||||
|
|
||||||
tokio::fs::write(&config_file_path, new_text)
|
|
||||||
.await
|
|
||||||
.context("Failed to update configuration file")?;
|
|
||||||
|
|
||||||
// clear the previously cached package.json from memory before reloading it
|
// clear the previously cached package.json from memory before reloading it
|
||||||
node_resolver::PackageJsonThreadLocalCache::clear();
|
node_resolver::PackageJsonThreadLocalCache::clear();
|
||||||
|
@ -524,7 +707,8 @@ impl AddPackageReq {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_imports(packages_to_version: Vec<(String, String)>) -> String {
|
fn generate_imports(mut packages_to_version: Vec<(String, String)>) -> String {
|
||||||
|
packages_to_version.sort_by(|(k1, _), (k2, _)| k1.cmp(k2));
|
||||||
let mut contents = vec![];
|
let mut contents = vec![];
|
||||||
let len = packages_to_version.len();
|
let len = packages_to_version.len();
|
||||||
for (index, (package, version)) in packages_to_version.iter().enumerate() {
|
for (index, (package, version)) in packages_to_version.iter().enumerate() {
|
||||||
|
@ -537,68 +721,27 @@ fn generate_imports(packages_to_version: Vec<(String, String)>) -> String {
|
||||||
contents.join("\n")
|
contents.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_from_config(
|
|
||||||
config_path: &Path,
|
|
||||||
keys: &[&'static str],
|
|
||||||
packages_to_remove: &[String],
|
|
||||||
removed_packages: &mut Vec<String>,
|
|
||||||
fmt_options: &FmtOptionsConfig,
|
|
||||||
) -> Result<(), AnyError> {
|
|
||||||
let mut json: serde_json::Value =
|
|
||||||
serde_json::from_slice(&std::fs::read(config_path)?)?;
|
|
||||||
for key in keys {
|
|
||||||
let Some(obj) = json.get_mut(*key).and_then(|v| v.as_object_mut()) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
for package in packages_to_remove {
|
|
||||||
if obj.shift_remove(package).is_some() {
|
|
||||||
removed_packages.push(package.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = serde_json::to_string_pretty(&json)?;
|
|
||||||
let config =
|
|
||||||
crate::tools::fmt::format_json(config_path, &config, fmt_options)
|
|
||||||
.ok()
|
|
||||||
.flatten()
|
|
||||||
.unwrap_or(config);
|
|
||||||
|
|
||||||
std::fs::write(config_path, config)
|
|
||||||
.context("Failed to update configuration file")?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn remove(
|
pub async fn remove(
|
||||||
flags: Arc<Flags>,
|
flags: Arc<Flags>,
|
||||||
remove_flags: RemoveFlags,
|
remove_flags: RemoveFlags,
|
||||||
) -> Result<(), AnyError> {
|
) -> Result<(), AnyError> {
|
||||||
let (config_file, factory) = DenoOrPackageJson::from_flags(flags.clone())?;
|
let (_, npm_config, deno_config) = load_configs(&flags)?;
|
||||||
let options = factory.cli_options()?;
|
|
||||||
let start_dir = &options.start_dir;
|
|
||||||
let fmt_config_options = config_file.fmt_options();
|
|
||||||
|
|
||||||
let mut removed_packages = Vec::new();
|
let mut configs = [
|
||||||
|
ConfigUpdater::maybe_new(npm_config).await?,
|
||||||
|
ConfigUpdater::maybe_new(deno_config).await?,
|
||||||
|
];
|
||||||
|
|
||||||
if let Some(deno_json) = start_dir.maybe_deno_json() {
|
let mut removed_packages = vec![];
|
||||||
remove_from_config(
|
|
||||||
&deno_json.specifier.to_file_path().unwrap(),
|
|
||||||
&["imports"],
|
|
||||||
&remove_flags.packages,
|
|
||||||
&mut removed_packages,
|
|
||||||
&fmt_config_options,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(pkg_json) = start_dir.maybe_pkg_json() {
|
for package in &remove_flags.packages {
|
||||||
remove_from_config(
|
let mut removed = false;
|
||||||
&pkg_json.path,
|
for config in configs.iter_mut().flatten() {
|
||||||
&["dependencies", "devDependencies"],
|
removed |= config.remove(package);
|
||||||
&remove_flags.packages,
|
}
|
||||||
&mut removed_packages,
|
if removed {
|
||||||
&fmt_config_options,
|
removed_packages.push(package.clone());
|
||||||
)?;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if removed_packages.is_empty() {
|
if removed_packages.is_empty() {
|
||||||
|
@ -607,6 +750,10 @@ pub async fn remove(
|
||||||
for package in &removed_packages {
|
for package in &removed_packages {
|
||||||
log::info!("Removed {}", crate::colors::green(package));
|
log::info!("Removed {}", crate::colors::green(package));
|
||||||
}
|
}
|
||||||
|
for config in configs.into_iter().flatten() {
|
||||||
|
config.commit().await?;
|
||||||
|
}
|
||||||
|
|
||||||
// Update deno.lock
|
// Update deno.lock
|
||||||
node_resolver::PackageJsonThreadLocalCache::clear();
|
node_resolver::PackageJsonThreadLocalCache::clear();
|
||||||
let cli_factory = CliFactory::from_flags(flags);
|
let cli_factory = CliFactory::from_flags(flags);
|
||||||
|
@ -616,41 +763,72 @@ pub async fn remove(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_config_file_content(
|
fn update_config_file_content<
|
||||||
obj: jsonc_parser::ast::Object,
|
I: IntoIterator<Item = (&'static str, Option<String>)>,
|
||||||
|
>(
|
||||||
|
obj: &jsonc_parser::ast::Object,
|
||||||
config_file_contents: &str,
|
config_file_contents: &str,
|
||||||
generated_imports: String,
|
|
||||||
fmt_options: FmtOptionsConfig,
|
fmt_options: FmtOptionsConfig,
|
||||||
imports_key: &str,
|
entries: I,
|
||||||
file_name: &str,
|
file_name: &str,
|
||||||
) -> String {
|
) -> String {
|
||||||
let mut text_changes = vec![];
|
let mut text_changes = vec![];
|
||||||
|
for (key, value) in entries {
|
||||||
|
match obj.properties.iter().enumerate().find_map(|(idx, k)| {
|
||||||
|
if k.name.as_str() == key {
|
||||||
|
Some((idx, k))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Some((
|
||||||
|
idx,
|
||||||
|
ObjectProp {
|
||||||
|
value: Value::Object(lit),
|
||||||
|
range,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
)) => {
|
||||||
|
if let Some(value) = value {
|
||||||
|
text_changes.push(TextChange {
|
||||||
|
range: (lit.range.start + 1)..(lit.range.end - 1),
|
||||||
|
new_text: value,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
text_changes.push(TextChange {
|
||||||
|
// remove field entirely, making sure to
|
||||||
|
// remove the comma if it's not the last field
|
||||||
|
range: range.start..(if idx == obj.properties.len() - 1 {
|
||||||
|
range.end
|
||||||
|
} else {
|
||||||
|
obj.properties[idx + 1].range.start
|
||||||
|
}),
|
||||||
|
new_text: "".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match obj.get(imports_key) {
|
// need to add field
|
||||||
Some(ObjectProp {
|
None => {
|
||||||
value: Value::Object(lit),
|
if let Some(value) = value {
|
||||||
..
|
let insert_position = obj.range.end - 1;
|
||||||
}) => text_changes.push(TextChange {
|
text_changes.push(TextChange {
|
||||||
range: (lit.range.start + 1)..(lit.range.end - 1),
|
range: insert_position..insert_position,
|
||||||
new_text: generated_imports,
|
// NOTE(bartlomieju): adding `\n` here to force the formatter to always
|
||||||
}),
|
// produce a config file that is multiline, like so:
|
||||||
None => {
|
// ```
|
||||||
let insert_position = obj.range.end - 1;
|
// {
|
||||||
text_changes.push(TextChange {
|
// "imports": {
|
||||||
range: insert_position..insert_position,
|
// "<package_name>": "<registry>:<package_name>@<semver>"
|
||||||
// NOTE(bartlomieju): adding `\n` here to force the formatter to always
|
// }
|
||||||
// produce a config file that is multiline, like so:
|
// }
|
||||||
// ```
|
new_text: format!("\"{key}\": {{\n {value} }}"),
|
||||||
// {
|
})
|
||||||
// "imports": {
|
}
|
||||||
// "<package_name>": "<registry>:<package_name>@<semver>"
|
}
|
||||||
// }
|
// we verified the shape of `imports`/`dependencies` above
|
||||||
// }
|
Some(_) => unreachable!(),
|
||||||
new_text: format!("\"{imports_key}\": {{\n {generated_imports} }}"),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
// we verified the shape of `imports`/`dependencies` above
|
|
||||||
Some(_) => unreachable!(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_text =
|
let new_text =
|
||||||
|
|
|
@ -97,7 +97,7 @@ url.workspace = true
|
||||||
winapi.workspace = true
|
winapi.workspace = true
|
||||||
x25519-dalek = { version = "2.0.0", features = ["static_secrets"] }
|
x25519-dalek = { version = "2.0.0", features = ["static_secrets"] }
|
||||||
x509-parser = "0.15.0"
|
x509-parser = "0.15.0"
|
||||||
yoke = { version = "0.7.4", features = ["derive"] }
|
yoke.workspace = true
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows-sys.workspace = true
|
windows-sys.workspace = true
|
||||||
|
|
46
tests/specs/add/package_json_and_deno_json/__test__.jsonc
Normal file
46
tests/specs/add/package_json_and_deno_json/__test__.jsonc
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"tempDir": true,
|
||||||
|
"tests": {
|
||||||
|
"npm_prefers_package_json": {
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"args": "add npm:@denotest/esm-basic @denotest/add npm:@denotest/say-hello",
|
||||||
|
"output": "add.out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
"eval",
|
||||||
|
"console.log(Deno.readTextFileSync('package.json').trim())"
|
||||||
|
],
|
||||||
|
"output": "npm_prefer_package.json.out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
"eval",
|
||||||
|
"console.log(Deno.readTextFileSync('deno.json').trim())"
|
||||||
|
],
|
||||||
|
"output": "npm_prefer_deno.json.out"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"only_creates_deno_json_if_no_config": {
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"args": ["eval", "Deno.removeSync('deno.json')"],
|
||||||
|
"output": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": "add npm:@denotest/esm-basic",
|
||||||
|
"output": "add_esm_basic.out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
"eval",
|
||||||
|
"try { Deno.statSync('deno.json'); console.log('bad'); } catch (e) { if (e instanceof Deno.errors.NotFound) { console.log('good'); } else { console.log('bad error', e); }}"
|
||||||
|
],
|
||||||
|
"output": "good\n"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
tests/specs/add/package_json_and_deno_json/add.out
Normal file
12
tests/specs/add/package_json_and_deno_json/add.out
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[UNORDERED_START]
|
||||||
|
Add npm:@denotest/esm-basic@1.0.0
|
||||||
|
Add jsr:@denotest/add@1.0.0
|
||||||
|
Add npm:@denotest/say-hello@1.0.0
|
||||||
|
Download http://localhost:4260/@denotest/esm-basic
|
||||||
|
Download http://localhost:4260/@denotest/esm-basic/1.0.0.tgz
|
||||||
|
Download http://localhost:4260/@denotest/say-hello
|
||||||
|
Download http://localhost:4260/@denotest/say-hello/1.0.0.tgz
|
||||||
|
Initialize @denotest/esm-basic@1.0.0
|
||||||
|
Initialize @denotest/say-hello@1.0.0
|
||||||
|
Download http://127.0.0.1:4250/@denotest/add/1.0.0/mod.ts
|
||||||
|
[UNORDERED_END]
|
|
@ -0,0 +1,4 @@
|
||||||
|
Add npm:@denotest/esm-basic@1.0.0
|
||||||
|
Download http://localhost:4260/@denotest/esm-basic
|
||||||
|
Download http://localhost:4260/@denotest/esm-basic/1.0.0.tgz
|
||||||
|
Initialize @denotest/esm-basic@1.0.0
|
1
tests/specs/add/package_json_and_deno_json/deno.json
Normal file
1
tests/specs/add/package_json_and_deno_json/deno.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"@denotest/add": "jsr:@denotest/add@^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@denotest/esm-basic": "^1.0.0",
|
||||||
|
"@denotest/say-hello": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
1
tests/specs/add/package_json_and_deno_json/package.json
Normal file
1
tests/specs/add/package_json_and_deno_json/package.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -12,5 +12,8 @@
|
||||||
}, {
|
}, {
|
||||||
"args": ["eval", "console.log(Deno.readTextFileSync('deno.lock').trim())"],
|
"args": ["eval", "console.log(Deno.readTextFileSync('deno.lock').trim())"],
|
||||||
"output": "remove_lock.out"
|
"output": "remove_lock.out"
|
||||||
|
}, {
|
||||||
|
"args": ["eval", "console.log(Deno.readTextFileSync('deno.json').trim())"],
|
||||||
|
"output": "{\n}\n"
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
27
tests/specs/remove/package_json/__test__.jsonc
Normal file
27
tests/specs/remove/package_json/__test__.jsonc
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"tempDir": true,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"args": "remove @denotest/add",
|
||||||
|
"output": "rm_add.out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
"eval",
|
||||||
|
"console.log(Deno.readTextFileSync('package.json').trim())"
|
||||||
|
],
|
||||||
|
"output": "rm_add_package.json.out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": "remove @denotest/esm-basic",
|
||||||
|
"output": "rm_esm_basic.out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
"eval",
|
||||||
|
"console.log(Deno.readTextFileSync('package.json').trim())"
|
||||||
|
],
|
||||||
|
"output": "rm_esm_basic_package.json.out"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
4
tests/specs/remove/package_json/package.json
Normal file
4
tests/specs/remove/package_json/package.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"dependencies": { "@denotest/add": "^1.0.0" },
|
||||||
|
"devDependencies": { "@denotest/esm-basic": "^1.0.0" }
|
||||||
|
}
|
4
tests/specs/remove/package_json/rm_add.out
Normal file
4
tests/specs/remove/package_json/rm_add.out
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
Removed @denotest/add
|
||||||
|
Download http://localhost:4260/@denotest/esm-basic
|
||||||
|
Download http://localhost:4260/@denotest/esm-basic/1.0.0.tgz
|
||||||
|
Initialize @denotest/esm-basic@1.0.0
|
3
tests/specs/remove/package_json/rm_add_package.json.out
Normal file
3
tests/specs/remove/package_json/rm_add_package.json.out
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"devDependencies": { "@denotest/esm-basic": "^1.0.0" }
|
||||||
|
}
|
1
tests/specs/remove/package_json/rm_esm_basic.out
Normal file
1
tests/specs/remove/package_json/rm_esm_basic.out
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Removed @denotest/esm-basic
|
|
@ -0,0 +1,2 @@
|
||||||
|
{
|
||||||
|
}
|
Loading…
Reference in a new issue