use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use anyhow::anyhow;
use anyhow::bail;
use deno_terminal::colors;
use dprint_core::async_runtime::future;
use dprint_core::plugins;
use std::collections::HashMap;
use std::collections::HashSet;
use std::ffi::OsString;
use std::path::Path;
use std::path::PathBuf;
use std::rc::Rc;
use url::Url;
use crate::arg_parser::CliArgs;
use crate::arg_parser::FilePatternArgs;
use crate::arg_parser::OutputResolvedConfigSubCommand;
use crate::configuration::GetInitConfigFileTextOptions;
use crate::configuration::get_init_config_file_text;
use crate::configuration::*;
use crate::environment::CanonicalizedPathBuf;
use crate::environment::Environment;
use crate::plugins::FetchNpmLatestInfo;
use crate::plugins::InfoFilePluginInfo;
use crate::plugins::PluginResolver;
use crate::plugins::PluginSourceReference;
use crate::plugins::PluginWrapper;
use crate::plugins::detect_npm_plugin_kind_in_node_modules;
use crate::plugins::fetch_npm_latest_info;
use crate::plugins::read_info_file;
use crate::plugins::read_update_url;
use crate::plugins::resolve_npm_latest_version;
use crate::resolution::GetPluginResult;
use crate::resolution::ResolvePluginsScopeAndPathsOptions;
use crate::resolution::resolve_plugins_scope;
use crate::resolution::resolve_plugins_scope_and_paths;
use crate::utils::CachedDownloader;
use crate::utils::PathSource;
use crate::utils::PluginKind;
use crate::utils::pretty_print_json_text;
pub struct InitConfigFileOptions<'a> {
pub global: bool,
pub config_arg: Option<&'a str>,
pub non_interactive: bool,
}
pub async fn init_config_file(environment: &impl Environment, options: InitConfigFileOptions<'_>) -> Result<()> {
fn get_config_paths(environment: &impl Environment, options: &InitConfigFileOptions<'_>) -> Result<Vec<PathBuf>> {
if options.global {
let directory = crate::configuration::resolve_global_config_dir(environment).with_context(|| {
concat!(
"Could not find system config directory. ",
"Maybe specify the DPRINT_CONFIG_DIR environment ",
"variable to say where to store the global dprint configuration file."
)
})?;
Ok(Vec::from([directory.join("dprint.jsonc"), directory.join("dprint.json")]))
} else if let Some(config_arg) = options.config_arg {
Ok(Vec::from([PathBuf::from(config_arg)]))
} else {
Ok(POSSIBLE_CONFIG_FILE_NAMES.iter().map(PathBuf::from).collect::<Vec<_>>())
}
}
let mut config_file_paths = get_config_paths(environment, &options)?;
for config_path in &config_file_paths {
if environment.path_exists(config_path) {
bail!("Configuration file '{}' already exists.", config_path.display())
}
}
let config_file_path = config_file_paths.remove(0);
let non_interactive = options.non_interactive || !environment.is_terminal_interactive();
let text = get_init_config_file_text(environment, GetInitConfigFileTextOptions { non_interactive }).await?;
if let Some(parent) = config_file_path.parent() {
_ = environment.mk_dir_all(parent);
}
environment.write_file(&config_file_path, &text)?;
log_stdout_info!(environment, "\nCreated {}", config_file_path.display());
if options.global {
log_stdout_info!(environment, "\nRun `dprint config edit --global` to modify this file in the future.");
}
log_stdout_info!(
environment,
"\nIf you are working in a commercial environment please consider sponsoring dprint: https://dprint.dev/sponsor"
);
Ok(())
}
pub async fn edit_config_file<TEnvironment: Environment>(args: &CliArgs, environment: &TEnvironment) -> Result<()> {
let config_path_and_bytes = resolve_main_config_path_and_bytes(args, environment).await?.ok_or_else(|| {
let is_global = args.config_discovery(environment).is_global();
anyhow::anyhow!(
"Could not find a configuration file. Create one with `dprint init{}`",
if is_global { " --global" } else { "" }
)
})?;
let config_path = match config_path_and_bytes.source {
PathSource::Local(source) => source.path,
PathSource::Remote(source) => {
bail!("Cannot edit a remote configuration file '{}'", source.url)
}
PathSource::Npm(source) => {
bail!("Cannot edit an npm configuration '{}'", source.specifier.display())
}
};
let args = select_editor_args(environment)
.into_iter()
.map(OsString::from)
.chain(std::iter::once(config_path.into_path_buf().into_os_string()))
.collect::<Vec<OsString>>();
let command_text_for_err = args
.iter()
.map(|s| format!("\"{}\"", s.to_string_lossy().replace("\"", "\\\"")))
.collect::<Vec<_>>()
.join(" ");
let exit_code = environment
.run_command_get_status(args)
.with_context(|| format!("Failed to launch editor with command: {}", command_text_for_err))?;
if let Some(exit_code) = exit_code.filter(|c| *c != 0) {
bail!("Editor exited with code: {}", exit_code);
}
Ok(())
}
pub struct AddPluginsOptions<'a> {
pub plugin_names_or_urls: &'a [String],
pub no_version: bool,
pub update_package_json: bool,
pub checksum: bool,
}
pub async fn add_plugin_config_file<TEnvironment: Environment>(
args: &CliArgs,
options: AddPluginsOptions<'_>,
environment: &TEnvironment,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
) -> Result<()> {
let AddPluginsOptions {
plugin_names_or_urls,
no_version,
update_package_json,
checksum,
} = options;
let config = resolve_config_from_args(args, environment).await?;
let config_path = match config.source {
PathSource::Local(source) => source.path,
PathSource::Remote(_) | PathSource::Npm(_) => bail!("Cannot update plugins in a remote configuration."),
};
let mut package_json_additions: Vec<(String, String)> = Vec::new();
let mut npm_packages_to_replace: Vec<String> = Vec::new();
let plugin_urls_to_add = if plugin_names_or_urls.is_empty() {
if no_version || update_package_json {
bail!("--no-version / --package-json require an explicit `npm:` specifier.");
}
let mut possible_plugins = get_possible_plugins_to_add(environment, plugin_resolver, config.plugins).await?;
if possible_plugins.is_empty() {
bail!("Could not find any plugins to add. Please provide one by specifying `dprint add <plugin-url>`.");
}
let index = environment.get_selection(
"Select a plugin to add:",
0,
&possible_plugins.iter().map(|p| p.name.clone()).collect::<Vec<_>>(),
)?;
let selected = possible_plugins.remove(index);
let url = if checksum {
ensure_url_checksum(selected.full_url(), plugin_resolver).await?
} else {
selected.full_url_no_wasm_checksum()
};
vec![url]
} else {
let mut urls = Vec::with_capacity(plugin_names_or_urls.len());
for plugin_name_or_url in plugin_names_or_urls {
if let Some(resolved) = resolve_plugin_url_to_add(
ResolvePluginUrlOptions {
plugin_name_or_url,
config_path: &config_path,
config_plugins: &config.plugins,
no_version,
update_package_json,
checksum,
},
&mut package_json_additions,
&mut npm_packages_to_replace,
environment,
plugin_resolver,
)
.await?
{
urls.push(resolved);
}
}
urls
};
let file_text = environment.read_file(&config_path)?;
let file_text = add_plugins_to_config(&file_text, &npm_packages_to_replace, &plugin_urls_to_add)?;
environment.write_file(&config_path, &file_text)?;
if update_package_json && !package_json_additions.is_empty() {
apply_package_json_additions(&config_path, &package_json_additions, environment)?;
}
Ok(())
}
struct ResolvePluginUrlOptions<'a> {
plugin_name_or_url: &'a str,
config_path: &'a CanonicalizedPathBuf,
config_plugins: &'a [PluginSourceReference],
no_version: bool,
update_package_json: bool,
checksum: bool,
}
#[derive(Debug)]
struct ResolvedNpmPluginAdd {
url: String,
package_name: String,
package_json_addition: Option<(String, String)>,
}
async fn resolve_plugin_url_to_add<TEnvironment: Environment>(
options: ResolvePluginUrlOptions<'_>,
package_json_additions: &mut Vec<(String, String)>,
npm_packages_to_replace: &mut Vec<String>,
environment: &TEnvironment,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
) -> Result<Option<String>> {
let ResolvePluginUrlOptions {
plugin_name_or_url,
config_path,
config_plugins,
no_version,
update_package_json,
checksum,
} = options;
if plugin_name_or_url.starts_with("npm:") {
let resolved = resolve_npm_plugin_to_add(
ResolveNpmPluginOptions {
text: plugin_name_or_url,
config_path,
no_version,
update_package_json,
checksum,
},
environment,
plugin_resolver,
)
.await?;
if let Some(addition) = resolved.package_json_addition {
package_json_additions.push(addition);
}
npm_packages_to_replace.push(resolved.package_name);
return Ok(Some(resolved.url));
}
if no_version || update_package_json {
bail!("--no-version / --package-json only apply to `npm:` specifiers (got '{}').", plugin_name_or_url);
}
match Url::parse(plugin_name_or_url) {
Ok(url) => {
let url = url.to_string();
let url = if checksum { ensure_url_checksum(url, plugin_resolver).await? } else { url };
Ok(Some(url))
}
Err(_) => {
let cached_downloader = CachedDownloader::new(environment.clone());
let plugin_name = if plugin_name_or_url.contains('/') {
plugin_name_or_url.to_string()
} else {
format!("dprint/{}", plugin_name_or_url)
};
let plugin = match read_update_url(
&cached_downloader,
&Url::parse(&format!("https://plugins.dprint.dev/{}/latest.json", plugin_name))?,
)
.await?
{
Some(result) => result,
None => {
let trailing_message = if let Ok(possible_plugins) = get_possible_plugins_to_add(environment, plugin_resolver, config_plugins.to_vec()).await {
if possible_plugins.is_empty() {
String::new()
} else {
format!(
"\n\nPlugins:\n{}",
possible_plugins.iter().map(|p| format!(" * {}", p.name)).collect::<Vec<_>>().join("\n")
)
}
} else {
String::new()
};
bail!(
"Could not find plugin with name '{}'. Please fix the name or try a url instead.{}",
plugin_name_or_url,
trailing_message,
)
}
};
for (config_plugin_reference, config_plugin) in get_config_file_plugins(plugin_resolver, config_plugins.to_vec()).await {
if let Ok(config_plugin) = config_plugin
&& let Some(update_url) = &config_plugin.info().update_url
&& let Ok(update_url) = Url::parse(update_url)
&& let Ok(Some(config_plugin_latest)) = read_update_url(&cached_downloader, &update_url).await
{
if config_plugin_latest.url == plugin.url {
let file_text = environment.read_file(config_path)?;
let new_reference = plugin.as_source_reference()?;
let file_text = update_plugin_in_config(
&file_text,
&PluginUpdateInfo {
name: config_plugin.info().name.to_string(),
old_version: config_plugin.info().version.to_string(),
old_reference: config_plugin_reference,
new_version: plugin.version,
new_reference,
},
);
environment.write_file(config_path, &file_text)?;
return Ok(None);
}
}
}
if checksum {
Ok(Some(ensure_url_checksum(plugin.full_url(), plugin_resolver).await?))
} else {
Ok(Some(plugin.full_url_no_wasm_checksum()))
}
}
}
}
async fn ensure_url_checksum<TEnvironment: Environment>(url: String, plugin_resolver: &Rc<PluginResolver<TEnvironment>>) -> Result<String> {
let parsed = crate::utils::parse_checksum_path_or_url(&url);
if parsed.checksum.is_some() {
return Ok(url);
}
let lower = parsed.path_or_url.to_lowercase();
if !(lower.ends_with(".wasm") || lower.ends_with(".json")) {
return Ok(url); }
let reference = PluginSourceReference {
path_source: PathSource::new_remote(Url::parse(&parsed.path_or_url)?),
checksum: None,
};
let checksum = plugin_resolver.resolve_remote_for_add(&reference).await?;
Ok(format!("{}@{}", parsed.path_or_url, checksum))
}
struct ResolveNpmPluginOptions<'a> {
text: &'a str,
config_path: &'a CanonicalizedPathBuf,
no_version: bool,
update_package_json: bool,
checksum: bool,
}
async fn resolve_npm_plugin_to_add<TEnvironment: Environment>(
options: ResolveNpmPluginOptions<'_>,
environment: &TEnvironment,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
) -> Result<ResolvedNpmPluginAdd> {
let ResolveNpmPluginOptions {
text,
config_path,
no_version,
update_package_json,
checksum,
} = options;
let parsed = crate::utils::parse_npm_specifier(text)?;
let explicit_path = parsed.path_was_explicit;
if explicit_path {
crate::utils::validate_plugin_extension(&parsed.specifier, text)?;
}
let start_dir = config_path
.parent()
.ok_or_else(|| anyhow!("Config path {} has no parent directory.", config_path.display()))?;
let start_dir_ref: &Path = start_dir.as_ref();
let name = parsed.specifier.name.clone();
if let Some(version) = &parsed.specifier.version {
if no_version {
bail!("--no-version cannot be combined with a versioned specifier: {}", text);
}
if parsed.checksum.is_some() || (explicit_path && !checksum) {
return Ok(ResolvedNpmPluginAdd {
url: text.to_string(),
package_name: name,
package_json_addition: None,
});
}
let resolution = plugin_resolver.resolve_npm_for_add(&parsed.specifier, explicit_path, Some(&start_dir)).await?;
let pinned = crate::utils::NpmSpecifier {
name: name.clone(),
version: Some(version.clone()),
path: resolution.path,
};
return Ok(ResolvedNpmPluginAdd {
url: npm_add_url(&pinned, resolution.plugin_kind, &resolution.checksum, checksum),
package_name: name,
package_json_addition: None,
});
}
if no_version {
let package_json_addition = if update_package_json {
let info = fetch_npm_latest_info(
FetchNpmLatestInfo {
specifier: &parsed.specifier,
start_dir: Some(start_dir_ref),
want_tarball_sha: false,
},
environment,
)
.await
.with_context(|| format!("Resolving latest version for package.json entry of {}", name))?;
Some((name.clone(), format!("^{}", info.version)))
} else {
None
};
return Ok(ResolvedNpmPluginAdd {
url: unversioned_npm_add_url(&parsed, explicit_path, start_dir_ref, environment),
package_name: name,
package_json_addition,
});
}
if !checksum && is_in_package_json_deps(&name, start_dir_ref, environment) {
log_stderr_info!(environment, "Found {} in package.json — adding unversioned npm specifier.", name);
return Ok(ResolvedNpmPluginAdd {
url: unversioned_npm_add_url(&parsed, explicit_path, start_dir_ref, environment),
package_name: name,
package_json_addition: None,
});
}
let version = resolve_npm_latest_version(&name, Some(start_dir_ref), environment).await?;
let needs_setup = !explicit_path || checksum || parsed.specifier.plugin_kind() == PluginKind::Process;
if !needs_setup {
let pinned = crate::utils::NpmSpecifier {
name: name.clone(),
version: Some(version),
path: parsed.specifier.path.clone(),
};
return Ok(ResolvedNpmPluginAdd {
url: pinned.display(),
package_name: name,
package_json_addition: None,
});
}
let versioned = crate::utils::NpmSpecifier {
name: name.clone(),
version: Some(version.clone()),
path: parsed.specifier.path.clone(),
};
let resolution = plugin_resolver.resolve_npm_for_add(&versioned, explicit_path, Some(&start_dir)).await?;
let pinned = crate::utils::NpmSpecifier {
name: name.clone(),
version: Some(version),
path: resolution.path,
};
Ok(ResolvedNpmPluginAdd {
url: npm_add_url(&pinned, resolution.plugin_kind, &resolution.checksum, checksum),
package_name: name,
package_json_addition: None,
})
}
fn npm_add_url(specifier: &crate::utils::NpmSpecifier, plugin_kind: PluginKind, tarball_sha256: &str, force_checksum: bool) -> String {
let display = specifier.display();
if force_checksum || plugin_kind == PluginKind::Process {
format!("{}@{}", display, tarball_sha256)
} else {
display
}
}
fn unversioned_npm_add_url(parsed: &crate::utils::ParsedNpmSpecifier, explicit_path: bool, start_dir: &Path, environment: &impl Environment) -> String {
if explicit_path {
return parsed.specifier.display();
}
let path = match detect_npm_plugin_kind_in_node_modules(&parsed.specifier.name, start_dir, environment) {
Some(PluginKind::Process) => "plugin.json".to_string(),
_ => parsed.specifier.path.clone(),
};
crate::utils::NpmSpecifier {
name: parsed.specifier.name.clone(),
version: None,
path,
}
.display()
}
fn apply_package_json_additions(config_path: &CanonicalizedPathBuf, additions: &[(String, String)], environment: &impl Environment) -> Result<()> {
use jsonc_parser::cst::CstRootNode;
use jsonc_parser::json;
let start_dir = config_path
.parent()
.ok_or_else(|| anyhow!("Config path {} has no parent directory.", config_path.display()))?;
let mut pkg_path = None;
for dir in start_dir.as_ref().ancestors() {
let candidate = dir.join("package.json");
if environment.path_exists(&candidate) {
pkg_path = Some(candidate);
break;
}
}
let Some(pkg_path) = pkg_path else {
log_warn!(
environment,
"Skipped package.json update: no package.json was found at or above {}. Run `npm init -y` and re-run `dprint add --package-json` to record {} entr{}.",
start_dir.display(),
additions.len(),
if additions.len() == 1 { "y" } else { "ies" },
);
return Ok(());
};
let text = environment.read_file(&pkg_path)?;
let root = CstRootNode::parse(&text, &Default::default()).with_context(|| format!("Failed parsing {}", pkg_path.display()))?;
let root_obj = root.object_value_or_set();
let dev_deps = root_obj.object_value_or_set("devDependencies");
dev_deps.ensure_multiline();
for (name, range) in additions {
match dev_deps.get(name) {
Some(existing) => existing.set_value(json!(range.clone())),
None => {
dev_deps.append(name, json!(range.clone()));
}
}
}
environment.write_file(&pkg_path, &root.to_string())?;
log_stderr_info!(
environment,
"Updated {} with {} new devDependencies entr{}. Run `npm install` to install them.",
pkg_path.display(),
additions.len(),
if additions.len() == 1 { "y" } else { "ies" },
);
Ok(())
}
fn is_in_package_json_deps(package_name: &str, start_dir: &std::path::Path, environment: &impl Environment) -> bool {
use jsonc_parser::JsonValue;
use jsonc_parser::parse_to_value;
for dir in start_dir.ancestors() {
let pkg_path = dir.join("package.json");
let Ok(text) = environment.read_file(&pkg_path) else {
continue;
};
let parsed = match parse_to_value(&text, &Default::default()) {
Ok(Some(JsonValue::Object(obj))) => obj,
Ok(_) => {
log_warn!(environment, "Skipping {}: top-level value is not an object.", pkg_path.display());
continue;
}
Err(err) => {
log_warn!(environment, "Skipping {}: failed to parse ({:#}).", pkg_path.display(), err);
continue;
}
};
for field in ["dependencies", "devDependencies"] {
if let Some(JsonValue::Object(deps)) = parsed.get(field)
&& deps.get(package_name).is_some()
{
return true;
}
}
}
false
}
async fn get_possible_plugins_to_add<TEnvironment: Environment>(
environment: &TEnvironment,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
current_plugins: Vec<PluginSourceReference>,
) -> Result<Vec<InfoFilePluginInfo>> {
let info_file = read_info_file(environment)
.await
.map_err(|err| anyhow!("Failed downloading info file. {:#}", err))?;
let current_plugin_names = get_config_file_plugins(plugin_resolver, current_plugins)
.await
.into_iter()
.filter_map(|(plugin_reference, plugin_result)| match plugin_result {
Ok(plugin) => Some(plugin.info().name.to_string()),
Err(err) => {
log_warn!(environment, "Failed resolving plugin: {}\n\n{:#}", plugin_reference.path_source.display(), err);
None
}
})
.collect::<HashSet<_>>();
Ok(
info_file
.latest_plugins
.into_iter()
.filter(|p| !current_plugin_names.contains(&p.name))
.collect(),
)
}
pub struct UpdatePluginsOptions {
pub yes_to_prompts: bool,
pub dry_run: bool,
}
pub async fn update_plugins_config_file<TEnvironment: Environment>(
args: &CliArgs,
environment: &TEnvironment,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
options: UpdatePluginsOptions,
) -> Result<()> {
let UpdatePluginsOptions { yes_to_prompts, dry_run } = options;
if !args.plugins.is_empty() {
bail!("Cannot specify plugins for this sub command. Sorry, too much work for me.");
}
let file_pattern_args = FilePatternArgs {
include_patterns: Vec::new(),
include_pattern_overrides: None,
exclude_patterns: Vec::new(),
exclude_pattern_overrides: None,
allow_node_modules: false,
no_gitignore: false,
only_staged: false,
only_dirty: false,
};
let config_discovery = args.config_discovery(environment);
let scopes = resolve_plugins_scope_and_paths(
args,
&file_pattern_args,
environment,
plugin_resolver,
ResolvePluginsScopeAndPathsOptions {
skip_traversal: config_discovery.is_global(),
},
)
.await?;
let mut plugin_responses = HashMap::new();
let mut updates_per_scope = HashMap::with_capacity(scopes.len());
let mut dry_run_texts = HashMap::new();
for (i, scope) in scopes.iter().enumerate() {
let is_main_config = i == 0;
let Some(config) = &scope.scope.config else {
continue;
};
let config_path = match &config.source {
PathSource::Local(source) => &source.path,
PathSource::Remote(_) | PathSource::Npm(_) => {
log_warn!(environment, "Skipping non-local configuration file: {}", config.source.display());
continue;
}
};
let mut file_text = environment.read_file(config_path)?;
let plugins_to_update = get_plugins_to_update(environment, plugin_resolver, config.plugins.clone()).await?;
let mut updated_plugins = Vec::with_capacity(plugins_to_update.len());
for result in plugins_to_update {
match result {
Ok(info) => {
let should_update = if info.is_wasm() || yes_to_prompts || dry_run {
true
} else if let Some(previous_response) = plugin_responses.get(&info.new_reference) {
*previous_response
} else {
log_all!(
environment,
"The process plugin {} {} has a new url: {}",
info.name,
info.old_version,
info.get_full_new_config_url(),
);
let response = environment.confirm("Do you want to update it?", false)?;
plugin_responses.insert(info.new_reference.clone(), response);
response
};
if should_update {
let in_config = if is_main_config {
String::new()
} else {
format!(" in {}", config_path.display())
};
if dry_run {
log_stderr_info!(
environment,
"Would update {} {}{} to {}.",
colors::bold(&info.name),
info.old_version,
in_config,
info.new_version
);
} else {
log_stderr_info!(
environment,
"Updating {} {}{} to {}...",
info.name,
info.old_version,
in_config,
info.new_version
);
}
file_text = update_plugin_in_config(&file_text, &info);
updated_plugins.push(info);
}
}
Err(err_info) => {
log_warn!(environment, "Failed updating plugin {}: {:#}", err_info.name, err_info.error);
}
}
}
updates_per_scope.insert(config_path.clone(), updated_plugins);
if dry_run {
dry_run_texts.insert(config_path.clone(), file_text);
} else {
environment.write_file(config_path, &file_text)?;
}
}
if dry_run {
return preview_plugin_config_updates(environment, plugin_resolver, &updates_per_scope, &dry_run_texts).await;
}
run_plugin_config_updates(environment, args, &file_pattern_args, plugin_resolver, &updates_per_scope)
.await
.with_context(|| "Failed running plugin config updates.".to_string())?;
Ok(())
}
async fn preview_plugin_config_updates<TEnvironment: Environment>(
environment: &TEnvironment,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
updates_per_scope: &HashMap<CanonicalizedPathBuf, Vec<PluginUpdateInfo>>,
dry_run_texts: &HashMap<CanonicalizedPathBuf, String>,
) -> Result<()> {
let mut any_updates = false;
let mut config_paths = updates_per_scope.keys().collect::<Vec<_>>();
config_paths.sort_by_key(|p| p.display().to_string());
for config_path in config_paths {
let updated_plugins = &updates_per_scope[config_path];
if updated_plugins.is_empty() {
continue;
}
any_updates = true;
let Some(mut file_text) = dry_run_texts.get(config_path).cloned() else {
continue;
};
let config_map = match deserialize_config_raw(&file_text) {
Ok(map) => map,
Err(err) => {
log_warn!(environment, "Failed deserializing config file '{}': {:#}", config_path.display(), err);
continue;
}
};
let mut all_diagnostics = Vec::new();
for update_info in updated_plugins {
let plugin = match plugin_resolver.resolve_plugin(update_info.new_reference.clone()).await {
Ok(plugin) => plugin,
Err(err) => {
log_warn!(environment, "Failed resolving {}. {:#}", update_info.name, err);
continue;
}
};
let config_key = &plugin.info().config_key;
let Some(plugin_config) = config_map.get(config_key).and_then(|c| c.as_object()).cloned() else {
continue;
};
let initialized_plugin = match plugin.initialize().await {
Ok(plugin) => plugin,
Err(err) => {
log_warn!(environment, "Failed initializing {}. {:#}", update_info.name, err);
continue;
}
};
let changes = match initialized_plugin
.check_config_updates(plugins::CheckConfigUpdatesMessage {
old_version: Some(update_info.old_version.clone()),
config: plugin_config,
})
.await
{
Ok(changes) => changes,
Err(err) => {
log_warn!(environment, "Failed applying update config changes for {}. {:#}", update_info.name, err);
continue;
}
};
if changes.is_empty() {
continue;
}
let result = apply_config_changes(&file_text, config_key, &changes);
all_diagnostics.extend(result.diagnostics);
file_text = result.new_text;
}
if !all_diagnostics.is_empty() {
log_warn!(environment, "Had diagnostics applying update config changes for {}:", config_path.display());
for diagnostic in &all_diagnostics {
log_warn!(environment, "* {}", diagnostic);
}
}
log_stdout_info!(
environment,
"\n{}\n{}",
colors::bold(format!("{} would be updated to:", config_path.display())),
file_text
);
}
if any_updates {
log_stderr_info!(environment, "\n{}", colors::gray("This was a dry run. No files were changed."));
} else {
log_stderr_info!(environment, "{}", colors::gray("No plugin updates available."));
}
Ok(())
}
async fn run_plugin_config_updates<TEnvironment: Environment>(
environment: &TEnvironment,
args: &CliArgs,
file_pattern_args: &FilePatternArgs,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
updates_per_scope: &HashMap<CanonicalizedPathBuf, Vec<PluginUpdateInfo>>,
) -> Result<()> {
let config_discovery = args.config_discovery(environment);
let scopes = resolve_plugins_scope_and_paths(
args,
file_pattern_args,
environment,
plugin_resolver,
ResolvePluginsScopeAndPathsOptions {
skip_traversal: config_discovery.is_global(),
},
)
.await?;
for scope in scopes.into_iter() {
let Some(config) = &scope.scope.config else {
continue;
};
let config_path = match &config.source {
PathSource::Local(source) => &source.path,
PathSource::Remote(_) | PathSource::Npm(_) => {
continue;
}
};
let updated_plugins = match updates_per_scope.get(config_path) {
Some(updates) => updates,
None => {
continue;
}
};
if updated_plugins.is_empty() {
continue;
}
let mut file_text = environment.read_file(config_path)?;
let config_map = match deserialize_config_raw(&file_text) {
Ok(map) => map,
Err(err) => {
log_warn!(environment, "Failed deserializing config file '{}': {:#}", config_path.display(), err);
continue;
}
};
let mut all_diagnostics = Vec::new();
for plugin in scope.scope.plugins.values() {
let Some(update_info) = updated_plugins
.iter()
.find(|info| info.name == plugin.info().name && info.new_version == plugin.info().version)
else {
continue;
};
log_debug!(environment, "Updating for {}", plugin.name());
let config_key = &plugin.info().config_key;
let Some(plugin_config) = config_map.get(config_key).and_then(|c| c.as_object()).cloned() else {
continue;
};
let initialized_plugin = match plugin.initialize().await {
Ok(plugin) => plugin,
Err(err) => {
log_warn!(environment, "Failed initializing {}. {:#}", update_info.name, err);
continue;
}
};
let changes = match initialized_plugin
.check_config_updates(plugins::CheckConfigUpdatesMessage {
old_version: Some(update_info.old_version.clone()),
config: plugin_config,
})
.await
{
Ok(changes) => changes,
Err(err) => {
log_warn!(environment, "Failed applying update config changes for {}. {:#}", update_info.name, err);
continue;
}
};
log_debug!(environment, "Had {} changes.", changes.len());
if changes.is_empty() {
continue;
}
let result = apply_config_changes(&file_text, config_key, &changes);
all_diagnostics.extend(result.diagnostics);
file_text = result.new_text;
}
if !all_diagnostics.is_empty() {
log_warn!(environment, "Had diagnostics applying update config changes for {}:", config_path.display());
for diagnostic in &all_diagnostics {
log_warn!(environment, "* {}", diagnostic);
}
}
environment.write_file(config_path, &file_text)?;
}
Ok(())
}
struct PluginUpdateError {
name: String,
error: Error,
}
async fn get_plugins_to_update<TEnvironment: Environment>(
environment: &TEnvironment,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
plugins: Vec<PluginSourceReference>,
) -> Result<Vec<Result<PluginUpdateInfo, PluginUpdateError>>> {
async fn resolve_plugin_update_info<TEnvironment: Environment>(
environment: &TEnvironment,
plugin_reference: PluginSourceReference,
plugin_result: Result<Rc<PluginWrapper>>,
) -> Option<Result<PluginUpdateInfo, PluginUpdateError>> {
let plugin = match plugin_result {
Ok(plugin) => plugin,
Err(error) => {
return Some(Err(PluginUpdateError {
name: plugin_reference.path_source.display(),
error,
}));
}
};
if let PathSource::Npm(npm_source) = &plugin_reference.path_source {
if npm_source.specifier.version.is_none() {
log_warn!(
environment,
"Skipping {} (unversioned npm specifier — update via your package manager).",
plugin.info().name
);
return None;
}
let start_dir = npm_source.base_dir.as_ref().map(|d| d.as_ref());
let args = FetchNpmLatestInfo {
specifier: &npm_source.specifier,
start_dir,
want_tarball_sha: plugin_reference.checksum.is_some(),
};
match fetch_npm_latest_info(args, environment).await {
Ok(info) => {
let new_specifier = crate::utils::NpmSpecifier {
name: npm_source.specifier.name.clone(),
version: Some(info.version.clone()),
path: npm_source.specifier.path.clone(),
};
let new_reference = PluginSourceReference {
path_source: PathSource::new_npm(new_specifier, npm_source.base_dir.clone()),
checksum: info.tarball_sha256,
};
return Some(Ok(PluginUpdateInfo {
name: plugin.info().name.to_string(),
old_version: plugin.info().version.to_string(),
old_reference: plugin_reference,
new_version: info.version,
new_reference,
}));
}
Err(err) => {
return Some(Err(PluginUpdateError {
name: plugin_reference.path_source.display(),
error: err,
}));
}
}
}
if let Some(plugin_update_url) = &plugin.info().update_url {
match Url::parse(plugin_update_url) {
Ok(update_url) => {
match read_update_url(environment, &update_url).await.and_then(|result| match result {
Some(info) => match info.as_source_reference() {
Ok(source_reference) => Ok((info, source_reference)),
Err(err) => Err(err),
},
None => Err(anyhow!("Failed downloading {} - 404 Not Found", update_url)),
}) {
Ok((info, new_reference)) => Some(Ok(PluginUpdateInfo {
name: plugin.info().name.to_string(),
old_reference: plugin_reference,
old_version: plugin.info().version.to_string(),
new_version: info.version,
new_reference,
})),
Err(err) => {
log_warn!(environment, "Failed reading plugin latest info. {:#}", err);
None
}
}
}
Err(err) => {
log_warn!(environment, "Failed reading plugin latest info. {:#}", err);
None
}
}
} else {
log_warn!(
environment,
"Skipping {} as it did not specify an update url. Please update manually.",
plugin.info().name
);
None
}
}
let config_file_plugins = get_config_file_plugins(plugin_resolver, plugins).await;
let tasks = config_file_plugins
.into_iter()
.map(|(plugin_reference, plugin_result)| {
let environment = environment.clone();
dprint_core::async_runtime::spawn(async move { resolve_plugin_update_info(&environment, plugin_reference, plugin_result).await })
})
.collect::<Vec<_>>();
let mut final_infos = Vec::with_capacity(tasks.len());
for result in future::join_all(tasks).await {
let maybe_info = result.unwrap();
if let Some(info) = maybe_info
&& info.as_ref().ok().map(|info| info.old_version != info.new_version).unwrap_or(true)
{
final_infos.push(info);
}
}
Ok(final_infos)
}
pub async fn output_resolved_config<TEnvironment: Environment>(
cmd: &OutputResolvedConfigSubCommand,
args: &CliArgs,
environment: &TEnvironment,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
) -> Result<()> {
let config = Rc::new(resolve_config_from_args(args, environment).await?);
let plugins_scope = resolve_plugins_scope(config, environment, plugin_resolver).await?;
plugins_scope.ensure_no_global_config_diagnostics()?;
let included_plugin_names = cmd.file_path.as_ref().map(|file_path| {
let file_path = environment.cwd().join(file_path);
plugins_scope
.plugin_name_maps
.get_plugin_names_from_file_path(&file_path)
.into_iter()
.collect::<HashSet<_>>()
});
let mut plugin_jsons = Vec::new();
for plugin in plugins_scope.plugins.values() {
if let Some(included_plugin_names) = &included_plugin_names
&& !included_plugin_names.contains(plugin.name())
{
continue;
}
let config_key = &plugin.info().config_key;
let plugin = match plugin.get_or_create_checking_config_diagnostics(environment).await? {
GetPluginResult::HadDiagnostics(count) => bail!("Plugin had {} diagnostic(s)", count),
GetPluginResult::Success(plugin) => plugin,
};
let text = plugin.resolved_config().await?;
let pretty_text = pretty_print_json_text(&text)?;
plugin_jsons.push(format!("\"{}\": {}", config_key, pretty_text));
}
environment.log_machine_readable(
&if plugin_jsons.is_empty() {
"{}".to_string()
} else {
let text = plugin_jsons.join(",\n").lines().map(|l| format!(" {}", l)).collect::<Vec<_>>().join("\n");
format!("{{\n{}\n}}", text)
}
.into_bytes(),
);
Ok(())
}
async fn get_config_file_plugins<TEnvironment: Environment>(
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
current_plugins: Vec<PluginSourceReference>,
) -> Vec<(PluginSourceReference, Result<Rc<PluginWrapper>>)> {
let tasks = current_plugins
.into_iter()
.map(|plugin_reference| {
let plugin_resolver = plugin_resolver.clone();
dprint_core::async_runtime::spawn(async move {
let resolve_result = plugin_resolver.resolve_plugin(plugin_reference.clone()).await;
(plugin_reference, resolve_result)
})
})
.collect::<Vec<_>>();
let mut results = Vec::with_capacity(tasks.len());
for result in future::join_all(tasks).await {
results.push(result.unwrap());
}
results
}
fn select_editor_args(env: &impl Environment) -> Vec<String> {
fn try_parse_env_var(env: &impl Environment, name: &str) -> Option<Vec<String>> {
let var = env.env_var(name).filter(|v| !v.is_empty()).and_then(|v| v.into_string().ok())?;
match crate::utils::parse_command_line(&var) {
Ok(value) => Some(value),
Err(err) => {
log_warn!(env, "Failed resolving '{}' env var: {:#}", name, err);
None
}
}
}
if let Some(value) = try_parse_env_var(env, "DPRINT_EDITOR") {
return value;
}
if let Some(value) = try_parse_env_var(env, "VISUAL") {
return value;
}
if let Some(value) = try_parse_env_var(env, "EDITOR") {
return value;
}
if cfg!(windows) {
Vec::from(["notepad".to_string()])
} else {
Vec::from(["nano".to_string()])
}
}
#[cfg(test)]
mod test {
use std::path::Path;
use anyhow::Result;
use deno_terminal::colors;
use once_cell::sync::Lazy;
use pretty_assertions::assert_eq;
use serde_json::json;
use crate::assert_contains;
use crate::configuration::*;
use crate::environment::CanonicalizedPathBuf;
use crate::environment::Environment;
use crate::environment::TestEnvironment;
use crate::environment::TestEnvironmentBuilder;
use crate::environment::TestInfoFilePlugin;
use crate::test_helpers::TestProcessPluginFile;
use crate::test_helpers::TestProcessPluginFileBuilder;
use crate::test_helpers::get_test_wasm_plugin_checksum;
use crate::test_helpers::run_test_cli;
#[test]
fn should_initialize() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info
.add_plugin(TestInfoFilePlugin {
name: "dprint-plugin-typescript".to_string(),
version: "0.17.2".to_string(),
url: "https://plugins.dprint.dev/typescript-0.17.2.wasm".to_string(),
config_key: Some("typescript".to_string()),
file_extensions: vec!["ts".to_string()],
config_excludes: vec![],
..Default::default()
})
.add_plugin(TestInfoFilePlugin {
name: "dprint-plugin-jsonc".to_string(),
version: "0.2.3".to_string(),
url: "https://plugins.dprint.dev/json-0.2.3.wasm".to_string(),
config_key: Some("json".to_string()),
file_extensions: vec!["json".to_string()],
config_excludes: vec![],
..Default::default()
});
})
.build();
let expected_text = environment.clone().run_in_runtime({
let environment = environment.clone();
async move {
let expected_text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
environment.clear_logs();
expected_text
}
});
run_test_cli(vec!["init"], &environment).unwrap();
assert_eq!(
environment.take_stderr_messages(),
vec!["Select plugins (space to toggle, type to filter, enter to finish):"]
);
assert_eq!(
environment.take_stdout_messages(),
vec![
"\nCreated dprint.json",
"\nIf you are working in a commercial environment please consider sponsoring dprint: https://dprint.dev/sponsor"
]
);
assert_eq!(environment.read_file("./dprint.json").unwrap(), expected_text);
}
#[test]
fn should_initialize_with_yes_flag_without_prompt() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info
.add_plugin(TestInfoFilePlugin {
name: "dprint-plugin-typescript".to_string(),
version: "0.17.2".to_string(),
url: "https://plugins.dprint.dev/typescript-0.17.2.wasm".to_string(),
config_key: Some("typescript".to_string()),
file_extensions: vec!["ts".to_string()],
config_excludes: vec![],
..Default::default()
})
.add_plugin(TestInfoFilePlugin {
name: "dprint-plugin-jsonc".to_string(),
version: "0.2.3".to_string(),
url: "https://plugins.dprint.dev/json-0.2.3.wasm".to_string(),
config_key: Some("json".to_string()),
file_extensions: vec!["json".to_string()],
config_excludes: vec![],
..Default::default()
});
})
.write_file("./file.ts", "")
.build();
run_test_cli(vec!["init", "--yes"], &environment).unwrap();
assert_eq!(environment.take_stderr_messages(), Vec::<String>::new());
assert_eq!(
environment.take_stdout_messages(),
vec![
"\nCreated dprint.json",
"\nIf you are working in a commercial environment please consider sponsoring dprint: https://dprint.dev/sponsor"
]
);
let created = environment.read_file("./dprint.json").unwrap();
assert!(created.contains("typescript-0.17.2.wasm"));
assert!(!created.contains("json-0.2.3.wasm"));
}
#[test]
fn should_use_dprint_config_init_as_alias() {
let environment = TestEnvironment::new();
let expected_text = environment.clone().run_in_runtime({
let environment = environment.clone();
async move {
let expected_text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
environment.clear_logs();
expected_text
}
});
run_test_cli(vec!["config", "init"], &environment).unwrap();
environment.take_stderr_messages();
environment.take_stdout_messages();
assert_eq!(environment.read_file("./dprint.json").unwrap(), expected_text);
}
#[test]
fn should_initialize_with_specified_config_path() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info.add_plugin(TestInfoFilePlugin {
name: "dprint-plugin-typescript".to_string(),
version: "0.17.2".to_string(),
url: "https://plugins.dprint.dev/typescript-0.17.2.wasm".to_string(),
config_key: Some("typescript".to_string()),
file_extensions: vec!["ts".to_string()],
config_excludes: vec![],
..Default::default()
});
})
.build();
let expected_text = environment.clone().run_in_runtime({
let environment = environment.clone();
async move {
let expected_text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
environment.clear_logs();
expected_text
}
});
run_test_cli(vec!["init", "--config", "./test.config.json"], &environment).unwrap();
assert_eq!(
environment.take_stderr_messages(),
vec!["Select plugins (space to toggle, type to filter, enter to finish):"]
);
assert_eq!(
environment.take_stdout_messages(),
vec![
"\nCreated ./test.config.json",
"\nIf you are working in a commercial environment please consider sponsoring dprint: https://dprint.dev/sponsor"
]
);
assert_eq!(environment.read_file("./test.config.json").unwrap(), expected_text);
}
#[test]
fn should_error_when_config_file_exists_on_initialize() {
let environment = TestEnvironmentBuilder::new()
.with_default_config(|c| {
c.add_includes("**/*.txt");
})
.build();
let error_message = run_test_cli(vec!["init"], &environment).err().unwrap();
assert_eq!(error_message.to_string(), "Configuration file 'dprint.json' already exists.");
}
#[test]
fn should_initialize_global_config() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info
.add_plugin(TestInfoFilePlugin {
name: "dprint-plugin-typescript".to_string(),
version: "0.17.2".to_string(),
url: "https://plugins.dprint.dev/typescript-0.17.2.wasm".to_string(),
config_key: Some("typescript".to_string()),
file_extensions: vec!["ts".to_string()],
config_excludes: vec![],
..Default::default()
})
.add_plugin(TestInfoFilePlugin {
name: "dprint-plugin-jsonc".to_string(),
version: "0.2.3".to_string(),
url: "https://plugins.dprint.dev/json-0.2.3.wasm".to_string(),
config_key: Some("json".to_string()),
file_extensions: vec!["json".to_string()],
config_excludes: vec![],
..Default::default()
});
})
.build();
let expected_text = environment.clone().run_in_runtime({
let environment = environment.clone();
async move {
let expected_text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
environment.clear_logs();
expected_text
}
});
run_test_cli(vec!["init", "--global"], &environment).unwrap();
assert_eq!(
environment.take_stderr_messages(),
vec!["Select plugins (space to toggle, type to filter, enter to finish):"]
);
let config_path = if std::env::consts::OS == "macos" {
Path::new("/home/.config/dprint")
} else {
Path::new("/config/dprint")
}
.join("dprint.jsonc");
assert_eq!(
environment.take_stdout_messages(),
vec![
format!("\nCreated {}", config_path.display()),
"\nRun `dprint config edit --global` to modify this file in the future.".to_string(),
"\nIf you are working in a commercial environment please consider sponsoring dprint: https://dprint.dev/sponsor".to_string()
]
);
assert_eq!(environment.read_file(config_path).unwrap(), expected_text);
}
#[test]
fn should_initialize_global_config_via_env_var() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info.add_plugin(TestInfoFilePlugin {
name: "dprint-plugin-typescript".to_string(),
version: "0.17.2".to_string(),
url: "https://plugins.dprint.dev/typescript-0.17.2.wasm".to_string(),
config_key: Some("typescript".to_string()),
file_extensions: vec!["ts".to_string()],
config_excludes: vec![],
..Default::default()
});
})
.build();
environment.set_env_var("DPRINT_CONFIG_DIR", Some("/custom/config"));
let expected_text = environment.clone().run_in_runtime({
let environment = environment.clone();
async move {
let expected_text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
environment.clear_logs();
expected_text
}
});
run_test_cli(vec!["init", "--global"], &environment).unwrap();
assert_eq!(
environment.take_stderr_messages(),
vec!["Select plugins (space to toggle, type to filter, enter to finish):"]
);
assert_eq!(
environment.take_stdout_messages(),
vec![
format!("\nCreated {}", Path::new("/custom/config").join("dprint.jsonc").display()),
"\nRun `dprint config edit --global` to modify this file in the future.".to_string(),
"\nIf you are working in a commercial environment please consider sponsoring dprint: https://dprint.dev/sponsor".to_string()
]
);
assert_eq!(environment.read_file("/custom/config/dprint.jsonc").unwrap(), expected_text);
}
#[test]
fn should_error_when_global_config_file_already_exists() {
let environment = TestEnvironmentBuilder::new().write_file("/config/dprint/dprint.json", "{}").build();
let error_message = run_test_cli(vec!["config", "init", "--global"], &environment).err().unwrap();
assert_eq!(
error_message.to_string(),
format!(
"Configuration file '{}' already exists.",
Path::new("/config/dprint").join("dprint.json").display()
)
);
}
#[test]
fn config_add() {
let old_wasm_url = "https://plugins.dprint.dev/test-plugin-0.1.0.wasm".to_string();
let new_wasm_url = "https://plugins.dprint.dev/test-plugin.wasm".to_string();
let old_ps_checksum = OLD_PROCESS_PLUGIN_FILE.checksum();
let old_ps_url = format!("https://plugins.dprint.dev/test-process.json@{}", old_ps_checksum);
let new_ps_url = "https://plugins.dprint.dev/test-plugin-3.json".to_string();
let new_ps_url_with_checksum = format!("{}@{}", new_ps_url, NEW_PROCESS_PLUGIN_FILE.checksum());
let select_plugin_msg = "Select a plugin to add:".to_string();
test_add(TestAddOptions {
add_arg: None,
config_has_wasm: false,
config_has_process: false,
remote_has_checksums: false,
expected_error: None,
expected_logs: vec![select_plugin_msg.clone()],
expected_urls: vec![new_wasm_url.clone()],
selection_result: Some(0),
});
test_add(TestAddOptions {
add_arg: None,
config_has_wasm: false,
config_has_process: false,
remote_has_checksums: true,
expected_error: None,
expected_logs: vec![select_plugin_msg.clone()],
expected_urls: vec![new_ps_url_with_checksum.clone()],
selection_result: Some(1),
});
test_add(TestAddOptions {
add_arg: None,
config_has_wasm: false,
config_has_process: false,
remote_has_checksums: false,
expected_error: None,
expected_logs: vec![select_plugin_msg.clone()],
expected_urls: vec![new_ps_url.clone()],
selection_result: Some(1),
});
test_add(TestAddOptions {
add_arg: None,
config_has_wasm: true,
config_has_process: false,
remote_has_checksums: false,
expected_error: None,
expected_logs: vec![select_plugin_msg.clone()],
expected_urls: vec![old_wasm_url.clone(), new_ps_url.clone()],
selection_result: Some(0),
});
test_add(TestAddOptions {
add_arg: None,
config_has_wasm: false,
config_has_process: true,
remote_has_checksums: false,
expected_error: None,
expected_logs: vec![select_plugin_msg.clone()],
expected_urls: vec![old_ps_url.clone(), new_wasm_url.clone()],
selection_result: Some(0),
});
test_add(TestAddOptions {
add_arg: None,
config_has_wasm: true,
config_has_process: true,
remote_has_checksums: false,
expected_error: Some("Could not find any plugins to add. Please provide one by specifying `dprint add <plugin-url>`."),
expected_logs: vec![],
expected_urls: vec![],
selection_result: Some(0),
});
test_add(TestAddOptions {
add_arg: Some("test-plugin"),
config_has_wasm: false,
config_has_process: false,
remote_has_checksums: false,
expected_error: None,
expected_logs: vec![],
expected_urls: vec![new_wasm_url.clone()],
selection_result: None,
});
test_add(TestAddOptions {
add_arg: Some("my-plugin"),
config_has_wasm: false,
config_has_process: false,
remote_has_checksums: false,
expected_error: Some(
"Could not find plugin with name 'my-plugin'. Please fix the name or try a url instead.\n\nPlugins:\n * test-plugin\n * test-process-plugin",
),
expected_logs: vec![],
expected_urls: vec![],
selection_result: None,
});
test_add(TestAddOptions {
add_arg: Some("test-plugin"),
config_has_wasm: true,
config_has_process: false,
remote_has_checksums: false,
expected_error: None,
expected_logs: vec![],
expected_urls: vec![
new_wasm_url,
],
selection_result: None,
});
test_add(TestAddOptions {
add_arg: Some("https://plugins.dprint.dev/my-plugin.wasm"),
config_has_wasm: false,
config_has_process: false,
remote_has_checksums: false,
expected_error: None,
expected_logs: vec![],
expected_urls: vec!["https://plugins.dprint.dev/my-plugin.wasm".to_string()],
selection_result: None,
});
}
#[derive(Debug)]
struct TestAddOptions {
add_arg: Option<&'static str>,
config_has_wasm: bool,
config_has_process: bool,
remote_has_checksums: bool,
selection_result: Option<usize>,
expected_error: Option<&'static str>,
expected_logs: Vec<String>,
expected_urls: Vec<String>,
}
#[track_caller]
fn test_add(options: TestAddOptions) {
let expected_logs = options.expected_logs.clone();
let expected_urls = options.expected_urls.clone();
let environment = get_setup_env(SetupEnvOptions {
config_has_wasm: options.config_has_wasm,
config_has_wasm_checksum: false,
config_has_process: options.config_has_process,
remote_has_wasm_checksum: options.remote_has_checksums,
remote_has_process_checksum: options.remote_has_checksums,
});
if let Some(selection_result) = options.selection_result {
environment.set_selection_result(selection_result);
}
let mut args = vec!["config", "add"];
if let Some(add_arg) = options.add_arg {
args.push(add_arg);
}
match run_test_cli(args, &environment) {
Ok(()) => {
assert!(options.expected_error.is_none());
}
Err(err) => {
assert_eq!(Some(err.to_string()), options.expected_error.map(ToOwned::to_owned));
}
}
assert_eq!(environment.take_stderr_messages(), expected_logs);
if options.expected_error.is_none() {
let expected_text = format!(
r#"{{
"plugins": [
{}
]
}}"#,
expected_urls.into_iter().map(|u| format!(" \"{}\"", u)).collect::<Vec<_>>().join(",\n")
);
assert_eq!(environment.read_file("./dprint.json").unwrap(), expected_text);
}
}
#[test]
fn config_add_multiple() {
let new_wasm_url = "https://plugins.dprint.dev/test-plugin.wasm";
let new_ps_url = "https://plugins.dprint.dev/test-plugin-3.json";
let environment = get_setup_env(SetupEnvOptions {
config_has_wasm: false,
config_has_wasm_checksum: false,
config_has_process: false,
remote_has_wasm_checksum: false,
remote_has_process_checksum: false,
});
run_test_cli(vec!["add", "test-plugin", "test-process-plugin"], &environment).unwrap();
let expected_text = format!(
r#"{{
"plugins": [
"{}",
"{}"
]
}}"#,
new_wasm_url, new_ps_url,
);
assert_eq!(environment.read_file("./dprint.json").unwrap(), expected_text);
}
#[test]
fn config_add_checksum_named_wasm_plugin() {
let environment = get_setup_env(SetupEnvOptions {
config_has_wasm: false,
config_has_wasm_checksum: false,
config_has_process: false,
remote_has_wasm_checksum: true,
remote_has_process_checksum: false,
});
run_test_cli(vec!["config", "add", "--checksum", "test-plugin"], &environment).unwrap();
let expected = format!("https://plugins.dprint.dev/test-plugin.wasm@{}", get_test_wasm_plugin_checksum());
let dprint_json = environment.read_file("./dprint.json").unwrap();
assert!(dprint_json.contains(&expected), "got: {dprint_json}");
let _ = environment.take_stderr_messages();
}
#[test]
fn config_add_global() {
let new_wasm_url = "https://plugins.dprint.dev/test-plugin.wasm".to_string();
let environment = get_setup_env(SetupEnvOptions {
config_has_wasm: false,
config_has_wasm_checksum: false,
config_has_process: false,
remote_has_wasm_checksum: false,
remote_has_process_checksum: false,
});
environment.mk_dir_all("/config/dprint").unwrap();
environment
.write_file(
"/config/dprint/dprint.json",
r#"{
"plugins": [
]
}"#,
)
.unwrap();
run_test_cli(vec!["config", "add", "--global", "test-plugin"], &environment).unwrap();
let expected_text = format!(
r#"{{
"plugins": [
"{}"
]
}}"#,
new_wasm_url
);
assert_eq!(environment.read_file("/config/dprint/dprint.json").unwrap(), expected_text);
}
#[test]
fn config_update_global() {
let old_wasm_url = "https://plugins.dprint.dev/test-plugin-0.1.0.wasm".to_string();
let new_wasm_url = "https://plugins.dprint.dev/test-plugin.wasm".to_string();
let environment = get_setup_env(SetupEnvOptions {
config_has_wasm: true,
config_has_wasm_checksum: false,
config_has_process: false,
remote_has_wasm_checksum: false,
remote_has_process_checksum: false,
});
environment.mk_dir_all("/config/dprint").unwrap();
environment
.write_file(
"/config/dprint/dprint.json",
&format!(
r#"{{
"plugins": [
"{}"
]
}}"#,
old_wasm_url
),
)
.unwrap();
run_test_cli(vec!["config", "update", "--global"], &environment).unwrap();
assert_eq!(
environment.take_stderr_messages(),
vec![
"Updating test-plugin 0.1.0 to 0.2.0...".to_string(),
"Compiling https://plugins.dprint.dev/test-plugin.wasm".to_string(),
]
);
let expected_text = format!(
r#"{{
"plugins": [
"{}"
]
}}"#,
new_wasm_url
);
assert_eq!(environment.read_file("/config/dprint/dprint.json").unwrap(), expected_text);
}
#[test]
fn config_update_should_always_upgrade_to_latest_plugins() {
let new_wasm_url = "https://plugins.dprint.dev/test-plugin.wasm".to_string();
let new_ps_url = "https://plugins.dprint.dev/test-plugin-3.json".to_string();
let new_ps_url_with_checksum = format!("{}@{}", new_ps_url, NEW_PROCESS_PLUGIN_FILE.checksum());
test_update(TestUpdateOptions {
config_has_wasm: false,
config_has_wasm_checksum: false,
config_has_process: true,
remote_has_wasm_checksum: true,
remote_has_process_checksum: true,
confirm_results: Vec::new(),
expected_logs: vec![
"Updating test-process-plugin 0.1.0 to 0.3.0...".to_string(),
"Extracting zip for test-process-plugin".to_string(),
],
expected_urls: vec![new_ps_url_with_checksum.clone()],
always_update: true,
on_error: None,
exit_code: 0,
});
test_update(TestUpdateOptions {
config_has_wasm: false,
config_has_wasm_checksum: false,
config_has_process: true,
remote_has_wasm_checksum: false,
remote_has_process_checksum: false,
confirm_results: Vec::new(),
expected_logs: vec!["Updating test-process-plugin 0.1.0 to 0.3.0...".to_string()],
expected_urls: vec![new_ps_url.clone()],
always_update: true,
on_error: Some(Box::new(|text| {
assert_contains!(
text,
"Error resolving plugin https://plugins.dprint.dev/test-plugin-3.json: The plugin must have a checksum specified for security reasons since it is not a Wasm plugin."
);
})),
exit_code: 12,
});
test_update(TestUpdateOptions {
config_has_wasm: false,
config_has_wasm_checksum: false,
config_has_process: true,
remote_has_wasm_checksum: false,
remote_has_process_checksum: true,
confirm_results: Vec::new(),
expected_logs: vec![
"Updating test-process-plugin 0.1.0 to 0.3.0...".to_string(),
"Extracting zip for test-process-plugin".to_string(),
],
expected_urls: vec![new_ps_url_with_checksum.clone()],
always_update: true,
on_error: None,
exit_code: 0,
});
test_update(TestUpdateOptions {
config_has_wasm: true,
config_has_wasm_checksum: false,
config_has_process: true,
remote_has_wasm_checksum: false,
remote_has_process_checksum: true,
confirm_results: Vec::new(),
expected_logs: vec![
"Updating test-plugin 0.1.0 to 0.2.0...".to_string(),
"Updating test-process-plugin 0.1.0 to 0.3.0...".to_string(),
"Compiling https://plugins.dprint.dev/test-plugin.wasm".to_string(),
"Extracting zip for test-process-plugin".to_string(),
],
expected_urls: vec![new_wasm_url.clone(), new_ps_url_with_checksum.clone()],
always_update: true,
on_error: None,
exit_code: 0,
});
}
#[test]
fn config_update_should_upgrade_to_latest_plugins() {
let new_wasm_url = "https://plugins.dprint.dev/test-plugin.wasm".to_string();
let new_wasm_url_with_checksum = format!("{}@{}", new_wasm_url, get_test_wasm_plugin_checksum());
let updating_message = "Updating test-plugin 0.1.0 to 0.2.0...".to_string();
let compiling_message = "Compiling https://plugins.dprint.dev/test-plugin.wasm".to_string();
test_update(TestUpdateOptions {
config_has_wasm: true,
config_has_wasm_checksum: true,
config_has_process: false,
remote_has_wasm_checksum: true,
remote_has_process_checksum: true,
confirm_results: Vec::new(),
expected_logs: vec![updating_message.clone(), compiling_message.clone()],
expected_urls: vec![new_wasm_url_with_checksum.clone()],
always_update: false,
on_error: None,
exit_code: 0,
});
test_update(TestUpdateOptions {
config_has_wasm: true,
config_has_wasm_checksum: true,
config_has_process: false,
remote_has_wasm_checksum: false,
remote_has_process_checksum: true,
confirm_results: Vec::new(),
expected_logs: vec![updating_message.clone(), compiling_message.clone()],
expected_urls: vec![new_wasm_url.clone()],
always_update: false,
on_error: None,
exit_code: 0,
});
test_update(TestUpdateOptions {
config_has_wasm: true,
config_has_wasm_checksum: false,
config_has_process: false,
remote_has_wasm_checksum: true,
remote_has_process_checksum: true,
confirm_results: Vec::new(),
expected_logs: vec![updating_message.clone(), compiling_message.clone()],
expected_urls: vec![new_wasm_url.clone()],
always_update: false,
on_error: None,
exit_code: 0,
});
test_update(TestUpdateOptions {
config_has_wasm: true,
config_has_wasm_checksum: false,
config_has_process: false,
remote_has_wasm_checksum: false,
remote_has_process_checksum: true,
confirm_results: Vec::new(),
expected_logs: vec![updating_message.clone(), compiling_message.clone()],
expected_urls: vec![new_wasm_url.clone()],
always_update: false,
on_error: None,
exit_code: 0,
});
let old_ps_checksum = TestProcessPluginFile::default().checksum();
let old_ps_url = format!("https://plugins.dprint.dev/test-process.json@{}", old_ps_checksum);
let new_ps_url = "https://plugins.dprint.dev/test-plugin-3.json".to_string();
let new_ps_url_with_checksum = format!("{}@{}", new_ps_url, NEW_PROCESS_PLUGIN_FILE.checksum());
test_update(TestUpdateOptions {
config_has_wasm: false,
config_has_wasm_checksum: false,
config_has_process: true,
remote_has_wasm_checksum: true,
remote_has_process_checksum: true,
confirm_results: vec![Ok(Some(true))],
expected_logs: vec![
format!("The process plugin test-process-plugin 0.1.0 has a new url: {}", new_ps_url_with_checksum),
"Do you want to update it? Y".to_string(),
"Updating test-process-plugin 0.1.0 to 0.3.0...".to_string(),
"Extracting zip for test-process-plugin".to_string(),
],
expected_urls: vec![new_ps_url_with_checksum.clone()],
always_update: false,
on_error: None,
exit_code: 0,
});
test_update(TestUpdateOptions {
config_has_wasm: false,
config_has_wasm_checksum: false,
config_has_process: true,
remote_has_wasm_checksum: false,
remote_has_process_checksum: false,
confirm_results: vec![Ok(Some(true))],
expected_logs: vec![
format!("The process plugin test-process-plugin 0.1.0 has a new url: {}", new_ps_url),
"Do you want to update it? Y".to_string(),
"Updating test-process-plugin 0.1.0 to 0.3.0...".to_string(),
],
expected_urls: vec![new_ps_url.clone()],
always_update: false,
on_error: Some(Box::new(|text| {
assert_contains!(
text,
"Error resolving plugin https://plugins.dprint.dev/test-plugin-3.json: The plugin must have a checksum specified for security reasons since it is not a Wasm plugin."
);
})),
exit_code: 12,
});
test_update(TestUpdateOptions {
config_has_wasm: false,
config_has_wasm_checksum: false,
config_has_process: true,
remote_has_wasm_checksum: false,
remote_has_process_checksum: false,
confirm_results: vec![Ok(None)],
expected_logs: vec![
format!("The process plugin test-process-plugin 0.1.0 has a new url: {}", new_ps_url),
"Do you want to update it? N".to_string(),
],
expected_urls: vec![old_ps_url.clone()],
always_update: false,
on_error: None,
exit_code: 0,
});
test_update(TestUpdateOptions {
config_has_wasm: true,
config_has_wasm_checksum: false,
config_has_process: true,
remote_has_wasm_checksum: false,
remote_has_process_checksum: true,
confirm_results: vec![Ok(Some(false))],
expected_logs: vec![
"Updating test-plugin 0.1.0 to 0.2.0...".to_string(),
format!("The process plugin test-process-plugin 0.1.0 has a new url: {}", new_ps_url_with_checksum),
"Do you want to update it? N".to_string(),
"Compiling https://plugins.dprint.dev/test-plugin.wasm".to_string(),
],
expected_urls: vec![new_wasm_url.clone(), old_ps_url.clone()],
always_update: false,
on_error: None,
exit_code: 0,
});
}
#[test]
fn config_update_plugin_config() {
let mut builder = get_setup_builder(SetupEnvOptions {
config_has_wasm: true,
config_has_wasm_checksum: false,
config_has_process: true,
remote_has_wasm_checksum: false,
remote_has_process_checksum: true,
});
builder.with_default_config(|config| {
config.add_config_section(
"testProcessPlugin",
r#"{
"should_add": {
},
"should_set": "other",
"should_remove": {},
"should_set_past_version": ""
}"#,
);
config.add_config_section(
"test-plugin",
r#"{
"should_add": {
},
"should_set": "other",
"should_remove": {},
"should_set_past_version": ""
}"#,
);
});
builder.with_local_config("/sub_folder/dprint.json", |config| {
config
.add_remote_process_plugin()
.add_remote_wasm_plugin_0_1_0()
.add_config_section(
"testProcessPlugin",
r#"{
"should_set": "asdf"
}"#,
)
.add_config_section(
"test-plugin",
r#"{
"should_set": "asdf"
}"#,
);
});
let environment = builder.initialize().build();
run_test_cli(vec!["config", "update", "--yes", "--recursive"], &environment).unwrap();
assert_eq!(
environment.take_stderr_messages(),
vec![
"Updating test-plugin 0.1.0 to 0.2.0...".to_string(),
"Updating test-process-plugin 0.1.0 to 0.3.0...".to_string(),
"Updating test-process-plugin 0.1.0 in /sub_folder/dprint.json to 0.3.0...".to_string(),
"Updating test-plugin 0.1.0 in /sub_folder/dprint.json to 0.2.0...".to_string(),
"Compiling https://plugins.dprint.dev/test-plugin.wasm".to_string(),
"Extracting zip for test-process-plugin".to_string()
]
);
assert_eq!(
environment.read_file("./dprint.json").unwrap(),
format!(
r#"{{
"testProcessPlugin": {{
"should_add": "new_value",
"should_set": "new_value",
"should_set_past_version": "0.1.0",
"new_prop1": ["new_value"],
"new_prop2": {{
"new_prop": "new_value"
}}
}},
"test-plugin": {{
"should_add": "new_value_wasm",
"should_set": "new_value_wasm",
"should_set_past_version": "0.1.0",
"new_prop1": ["new_value_wasm"],
"new_prop2": {{
"new_prop": "new_value_wasm"
}}
}},
"plugins": [
"https://plugins.dprint.dev/test-plugin.wasm",
"https://plugins.dprint.dev/test-plugin-3.json@{}"
]
}}"#,
NEW_PROCESS_PLUGIN_FILE.checksum()
)
);
assert_eq!(
environment.read_file("./sub_folder/dprint.json").unwrap(),
format!(
r#"{{
"testProcessPlugin": {{
"should_set": "new_value"
}},
"test-plugin": {{
"should_set": "new_value_wasm"
}},
"plugins": [
"https://plugins.dprint.dev/test-plugin-3.json@{}",
"https://plugins.dprint.dev/test-plugin.wasm"
]
}}"#,
NEW_PROCESS_PLUGIN_FILE.checksum()
)
);
}
#[test]
fn config_update_default_non_recursive() {
let mut builder = get_setup_builder(SetupEnvOptions {
config_has_wasm: true,
config_has_wasm_checksum: false,
config_has_process: true,
remote_has_wasm_checksum: false,
remote_has_process_checksum: true,
});
builder.with_default_config(|config| {
config.add_config_section(
"testProcessPlugin",
r#"{
"should_add": {
},
"should_set": "other",
"should_remove": {},
"should_set_past_version": ""
}"#,
);
config.add_config_section(
"test-plugin",
r#"{
"should_add": {
},
"should_set": "other",
"should_remove": {},
"should_set_past_version": ""
}"#,
);
});
builder.with_local_config("/sub_folder/dprint.json", |config| {
config
.add_remote_process_plugin()
.add_remote_wasm_plugin_0_1_0()
.add_config_section(
"testProcessPlugin",
r#"{
"should_set": "asdf"
}"#,
)
.add_config_section(
"test-plugin",
r#"{
"should_set": "asdf"
}"#,
);
});
let environment = builder.initialize().build();
run_test_cli(vec!["config", "update", "--yes"], &environment).unwrap();
assert_eq!(
environment.take_stderr_messages(),
vec![
"Updating test-plugin 0.1.0 to 0.2.0...".to_string(),
"Updating test-process-plugin 0.1.0 to 0.3.0...".to_string(),
"Compiling https://plugins.dprint.dev/test-plugin.wasm".to_string(),
"Extracting zip for test-process-plugin".to_string()
]
);
assert_eq!(
environment.read_file("./dprint.json").unwrap(),
format!(
r#"{{
"testProcessPlugin": {{
"should_add": "new_value",
"should_set": "new_value",
"should_set_past_version": "0.1.0",
"new_prop1": ["new_value"],
"new_prop2": {{
"new_prop": "new_value"
}}
}},
"test-plugin": {{
"should_add": "new_value_wasm",
"should_set": "new_value_wasm",
"should_set_past_version": "0.1.0",
"new_prop1": ["new_value_wasm"],
"new_prop2": {{
"new_prop": "new_value_wasm"
}}
}},
"plugins": [
"https://plugins.dprint.dev/test-plugin.wasm",
"https://plugins.dprint.dev/test-plugin-3.json@{}"
]
}}"#,
NEW_PROCESS_PLUGIN_FILE.checksum()
)
);
let old_ps_checksum = TestProcessPluginFile::default().checksum();
assert_eq!(
environment.read_file("./sub_folder/dprint.json").unwrap(),
format!(
r#"{{
"testProcessPlugin": {{
"should_set": "asdf"
}},
"test-plugin": {{
"should_set": "asdf"
}},
"plugins": [
"https://plugins.dprint.dev/test-process.json@{}",
"https://plugins.dprint.dev/test-plugin-0.1.0.wasm"
]
}}"#,
old_ps_checksum
)
);
}
#[test]
fn config_update_dry_run_does_not_modify_files() {
let mut builder = get_setup_builder(SetupEnvOptions {
config_has_wasm: true,
config_has_wasm_checksum: false,
config_has_process: true,
remote_has_wasm_checksum: false,
remote_has_process_checksum: true,
});
builder.with_default_config(|config| {
config.add_config_section(
"testProcessPlugin",
r#"{
"should_add": {
},
"should_set": "other",
"should_remove": {},
"should_set_past_version": ""
}"#,
);
config.add_config_section(
"test-plugin",
r#"{
"should_add": {
},
"should_set": "other",
"should_remove": {},
"should_set_past_version": ""
}"#,
);
});
builder.with_local_config("/sub_folder/dprint.json", |config| {
config
.add_remote_process_plugin()
.add_remote_wasm_plugin_0_1_0()
.add_config_section(
"testProcessPlugin",
r#"{
"should_set": "asdf"
}"#,
)
.add_config_section(
"test-plugin",
r#"{
"should_set": "asdf"
}"#,
);
});
let environment = builder.initialize().build();
let root_before = environment.read_file("./dprint.json").unwrap();
let sub_before = environment.read_file("./sub_folder/dprint.json").unwrap();
run_test_cli(vec!["config", "update", "--recursive", "--dry-run"], &environment).unwrap();
assert_eq!(
environment.take_stderr_messages(),
vec![
format!("Would update {} 0.1.0 to 0.2.0.", colors::bold("test-plugin")),
format!("Would update {} 0.1.0 to 0.3.0.", colors::bold("test-process-plugin")),
format!(
"Would update {} 0.1.0 in /sub_folder/dprint.json to 0.3.0.",
colors::bold("test-process-plugin")
),
format!("Would update {} 0.1.0 in /sub_folder/dprint.json to 0.2.0.", colors::bold("test-plugin")),
"Compiling https://plugins.dprint.dev/test-plugin.wasm".to_string(),
"Extracting zip for test-process-plugin".to_string(),
format!("\n{}", colors::gray("This was a dry run. No files were changed.")),
]
);
let stdout = environment.take_stdout_messages().join("\n");
assert_contains!(stdout, &colors::bold("/dprint.json would be updated to:").to_string());
assert_contains!(stdout, "\"should_add\": \"new_value\"");
assert_contains!(stdout, "\"should_add\": \"new_value_wasm\"");
assert_contains!(stdout, "https://plugins.dprint.dev/test-plugin.wasm");
assert_contains!(
stdout,
&format!("https://plugins.dprint.dev/test-plugin-3.json@{}", NEW_PROCESS_PLUGIN_FILE.checksum())
);
assert_eq!(environment.read_file("./dprint.json").unwrap(), root_before);
assert_eq!(environment.read_file("./sub_folder/dprint.json").unwrap(), sub_before);
}
struct TestUpdateOptions {
config_has_wasm: bool,
config_has_wasm_checksum: bool,
config_has_process: bool,
remote_has_wasm_checksum: bool,
remote_has_process_checksum: bool,
confirm_results: Vec<Result<Option<bool>>>,
expected_logs: Vec<String>,
expected_urls: Vec<String>,
always_update: bool,
on_error: Option<Box<dyn FnOnce(&str)>>,
exit_code: i32,
}
#[track_caller]
fn test_update(options: TestUpdateOptions) {
let expected_logs = options.expected_logs.clone();
let expected_urls = options.expected_urls.clone();
let environment = get_setup_env(SetupEnvOptions {
config_has_wasm: options.config_has_wasm,
config_has_wasm_checksum: options.config_has_wasm_checksum,
config_has_process: options.config_has_process,
remote_has_wasm_checksum: options.remote_has_wasm_checksum,
remote_has_process_checksum: options.remote_has_process_checksum,
});
environment.set_confirm_results(options.confirm_results);
let result = run_test_cli(
if options.always_update {
vec!["config", "update", "--yes"]
} else {
vec!["config", "update"]
},
&environment,
);
if let Err(err) = result {
let on_error = match options.on_error {
Some(on_error) => on_error,
None => panic!("{:#}", err),
};
(on_error)(&err.to_string());
err.assert_exit_code(options.exit_code);
} else {
assert_eq!(options.on_error.is_some(), false);
assert_eq!(options.exit_code, 0);
}
assert_eq!(environment.take_stderr_messages(), expected_logs);
let expected_text = format!(
r#"{{
"plugins": [
{}
]
}}"#,
expected_urls.into_iter().map(|u| format!(" \"{}\"", u)).collect::<Vec<_>>().join(",\n")
);
assert_eq!(environment.read_file("./dprint.json").unwrap(), expected_text);
}
static OLD_PROCESS_PLUGIN_FILE: Lazy<TestProcessPluginFile> = Lazy::new(|| TestProcessPluginFileBuilder::default().version("0.1.0").build());
static NEW_PROCESS_PLUGIN_FILE: Lazy<TestProcessPluginFile> = Lazy::new(|| TestProcessPluginFileBuilder::default().version("0.3.0").build());
#[derive(Debug)]
struct SetupEnvOptions {
config_has_wasm: bool,
config_has_wasm_checksum: bool,
config_has_process: bool,
remote_has_wasm_checksum: bool,
remote_has_process_checksum: bool,
}
fn get_setup_env(opts: SetupEnvOptions) -> TestEnvironment {
get_setup_builder(opts).initialize().build()
}
fn get_setup_builder(opts: SetupEnvOptions) -> TestEnvironmentBuilder {
let mut builder = TestEnvironmentBuilder::new();
if opts.config_has_wasm {
builder.add_remote_wasm_plugin();
builder.add_remote_wasm_0_1_0_plugin();
}
if opts.config_has_process {
builder.add_remote_process_plugin();
builder.add_remote_process_plugin_at_url("https://plugins.dprint.dev/test-plugin-3.json", &*NEW_PROCESS_PLUGIN_FILE);
}
builder
.with_info_file(|info| {
info.add_plugin(TestInfoFilePlugin {
name: "test-plugin".to_string(),
version: "0.2.0".to_string(),
url: "https://plugins.dprint.dev/test-plugin.wasm".to_string(),
config_key: Some("test-plugin".to_string()),
checksum: if opts.remote_has_wasm_checksum {
Some(get_test_wasm_plugin_checksum())
} else {
None
},
..Default::default()
});
info.add_plugin(TestInfoFilePlugin {
name: "test-process-plugin".to_string(),
version: "0.3.0".to_string(),
url: "https://plugins.dprint.dev/test-plugin-3.json".to_string(),
config_key: Some("test-process-plugin".to_string()),
checksum: if opts.remote_has_process_checksum {
Some(NEW_PROCESS_PLUGIN_FILE.checksum())
} else {
None
},
..Default::default()
});
})
.with_default_config(|config| {
config.ensure_plugins_section();
if opts.config_has_wasm {
if opts.config_has_wasm_checksum {
config.add_remote_wasm_plugin_0_1_0_with_checksum();
} else {
config.add_remote_wasm_plugin_0_1_0();
}
}
if opts.config_has_process {
config.add_remote_process_plugin();
}
})
.add_remote_file(
"https://plugins.dprint.dev/dprint/test-plugin/latest.json",
&json!({
"schemaVersion": 1,
"url": "https://plugins.dprint.dev/test-plugin.wasm",
"version": "0.2.0",
"checksum": if opts.remote_has_wasm_checksum { Some(get_test_wasm_plugin_checksum()) } else { None },
})
.to_string(),
)
.add_remote_file(
"https://plugins.dprint.dev/dprint/test-process-plugin/latest.json",
&json!({
"schemaVersion": 1,
"url": "https://plugins.dprint.dev/test-plugin-3.json",
"version": "0.3.0",
"checksum": if opts.remote_has_process_checksum { Some(NEW_PROCESS_PLUGIN_FILE.checksum()) } else { None },
})
.to_string(),
);
builder
}
#[test]
fn config_update_should_not_upgrade_when_at_latest_plugins() {
let environment = TestEnvironmentBuilder::new()
.add_remote_wasm_plugin()
.with_info_file(|_| {})
.with_default_config(|config| {
config.add_remote_wasm_plugin();
})
.add_remote_file(
"https://plugins.dprint.dev/dprint/test-plugin/latest.json",
&json!({
"schemaVersion": 1,
"url": "https://plugins.dprint.dev/test-plugin.wasm",
"version": "0.2.0"
})
.to_string(),
)
.initialize()
.build();
run_test_cli(vec!["config", "update"], &environment).unwrap();
assert!(environment.take_stderr_messages().is_empty());
}
#[test]
fn config_update_should_handle_wasm_to_process_plugin() {
let environment = TestEnvironmentBuilder::new()
.add_remote_wasm_plugin()
.add_remote_wasm_0_1_0_plugin()
.with_info_file(|_| {})
.with_default_config(|config| {
config.add_remote_wasm_plugin_0_1_0();
})
.add_remote_file(
"https://plugins.dprint.dev/dprint/test-plugin/latest.json",
&json!({
"schemaVersion": 1,
"url": "https://plugins.dprint.dev/test-plugin.json",
"version": "0.2.0",
"checksum": "checksum",
})
.to_string(),
)
.initialize()
.build();
environment.set_confirm_results(vec![Ok(None)]);
run_test_cli(vec!["config", "update"], &environment).unwrap();
assert_eq!(
environment.take_stderr_messages(),
vec![
"The process plugin test-plugin 0.1.0 has a new url: https://plugins.dprint.dev/test-plugin.json@checksum",
"Do you want to update it? N"
]
);
}
#[test]
fn should_output_resolved_config() {
let environment = TestEnvironmentBuilder::with_initialized_remote_wasm_and_process_plugin().build();
run_test_cli(vec!["output-resolved-config"], &environment).unwrap();
assert_eq!(
environment.take_stdout_messages(),
vec![concat!(
"{\n",
" \"test-plugin\": {\n",
" \"ending\": \"formatted\",\n",
" \"lineWidth\": 120\n",
" },\n",
" \"testProcessPlugin\": {\n",
" \"ending\": \"formatted_process\",\n",
" \"lineWidth\": 120\n",
" }\n",
"}",
)]
);
}
#[test]
fn should_output_resolved_config_for_file_path() {
let environment = TestEnvironmentBuilder::with_initialized_remote_wasm_and_process_plugin().build();
run_test_cli(vec!["resolved-config", "--file", "file.txt"], &environment).unwrap();
assert_eq!(
environment.take_stdout_messages(),
vec![concat!(
"{\n",
" \"test-plugin\": {\n",
" \"ending\": \"formatted\",\n",
" \"lineWidth\": 120\n",
" }\n",
"}",
)]
);
run_test_cli(vec!["resolved-config", "--file", "file.txt_ps"], &environment).unwrap();
assert_eq!(
environment.take_stdout_messages(),
vec![concat!(
"{\n",
" \"testProcessPlugin\": {\n",
" \"ending\": \"formatted_process\",\n",
" \"lineWidth\": 120\n",
" }\n",
"}",
)]
);
}
#[test]
fn should_output_empty_resolved_config_for_unhandled_file_path() {
let environment = TestEnvironmentBuilder::with_initialized_remote_wasm_and_process_plugin().build();
run_test_cli(vec!["resolved-config", "--file", "file.unknown_ext"], &environment).unwrap();
assert_eq!(environment.take_stdout_messages(), vec!["{}"]);
}
#[test]
fn should_output_resolved_config_for_file_path_with_associations() {
let environment = TestEnvironmentBuilder::with_initialized_remote_wasm_plugin()
.with_default_config(|c| {
c.add_remote_wasm_plugin().add_config_section(
"test-plugin",
r#"{
"associations": ["**/*.special"]
}"#,
);
})
.build();
let expected_handled = vec![concat!(
"{\n",
" \"test-plugin\": {\n",
" \"ending\": \"formatted\",\n",
" \"lineWidth\": 120\n",
" }\n",
"}",
)];
run_test_cli(vec!["resolved-config", "--file", "file.asdf"], &environment).unwrap();
assert_eq!(environment.take_stdout_messages(), vec!["{}"]);
run_test_cli(vec!["resolved-config", "--file", "file.txt"], &environment).unwrap();
assert_eq!(environment.take_stdout_messages(), expected_handled.clone());
run_test_cli(vec!["resolved-config", "--file", "file.special"], &environment).unwrap();
assert_eq!(environment.take_stdout_messages(), expected_handled);
}
#[test]
fn should_output_base_resolved_config_when_overrides_exist() {
let environment = TestEnvironmentBuilder::with_initialized_remote_wasm_plugin()
.with_default_config(|c| {
c.add_remote_wasm_plugin().add_config_section(
"test-plugin",
r#"{
"ending": "base",
"overrides": {
"files": "**/package.json",
"ending": "package"
}
}"#,
);
})
.build();
run_test_cli(vec!["output-resolved-config"], &environment).unwrap();
assert_eq!(
environment.take_stdout_messages(),
vec![concat!(
"{\n",
" \"test-plugin\": {\n",
" \"ending\": \"base\",\n",
" \"lineWidth\": 120\n",
" }\n",
"}",
)]
);
}
#[test]
fn should_error_for_override_config_diagnostics() {
let environment = TestEnvironmentBuilder::with_initialized_remote_wasm_plugin()
.with_default_config(|c| {
c.add_remote_wasm_plugin().add_config_section(
"test-plugin",
r#"{
"overrides": {
"files": "**/package.json",
"unknownProperty": true
}
}"#,
);
})
.build();
let err = run_test_cli(vec!["output-resolved-config"], &environment).err().unwrap();
assert_eq!(err.to_string(), "Plugin had 1 diagnostic(s)");
assert_eq!(
environment.take_stderr_messages(),
vec![
"[test-plugin]: Unknown property in configuration (unknownProperty)",
"[test-plugin]: Error initializing from configuration file. Had 1 diagnostic(s)."
]
);
}
#[test]
fn should_output_resolved_config_no_plugins() {
let environment = TestEnvironmentBuilder::new().with_default_config(|_| {}).build();
run_test_cli(vec!["output-resolved-config"], &environment).unwrap();
assert_eq!(environment.take_stdout_messages(), vec!["{}"]);
}
#[test]
fn config_edit_should_open_editor_with_local_config() {
let environment = TestEnvironmentBuilder::new()
.with_default_config(|config| {
config.add_includes("**/*.txt");
})
.build();
environment.set_run_command_result(Ok(Some(0)));
run_test_cli(vec!["config", "edit"], &environment).unwrap();
let commands = environment.take_run_commands();
assert_eq!(commands.len(), 1);
let (args, _) = &commands[0];
#[cfg(not(windows))]
{
assert_eq!(args.len(), 2);
assert_eq!(args[0], "nano");
assert_eq!(args[1], "/dprint.json");
}
#[cfg(windows)]
{
assert_eq!(args.len(), 2);
assert_eq!(args[0], "notepad");
assert_eq!(args[1], "/dprint.json");
}
}
#[test]
fn config_edit_should_use_dprint_editor_env_var() {
let environment = TestEnvironmentBuilder::new()
.with_default_config(|config| {
config.add_includes("**/*.txt");
})
.build();
environment.set_env_var("DPRINT_EDITOR", Some("vim"));
environment.set_run_command_result(Ok(Some(0)));
run_test_cli(vec!["config", "edit"], &environment).unwrap();
let commands = environment.take_run_commands();
assert_eq!(commands.len(), 1);
let (args, _) = &commands[0];
assert_eq!(args.len(), 2);
assert_eq!(args[0], "vim");
assert_eq!(args[1], "/dprint.json");
}
#[test]
fn config_edit_should_use_visual_env_var() {
let environment = TestEnvironmentBuilder::new()
.with_default_config(|config| {
config.add_includes("**/*.txt");
})
.build();
environment.set_env_var("VISUAL", Some("emacs"));
environment.set_run_command_result(Ok(Some(0)));
run_test_cli(vec!["config", "edit"], &environment).unwrap();
let commands = environment.take_run_commands();
assert_eq!(commands.len(), 1);
let (args, _) = &commands[0];
assert_eq!(args.len(), 2);
assert_eq!(args[0], "emacs");
assert_eq!(args[1], "/dprint.json");
}
#[test]
fn config_edit_should_use_editor_env_var() {
let environment = TestEnvironmentBuilder::new()
.with_default_config(|config| {
config.add_includes("**/*.txt");
})
.build();
environment.set_env_var("EDITOR", Some("vi"));
environment.set_run_command_result(Ok(Some(0)));
run_test_cli(vec!["config", "edit"], &environment).unwrap();
let commands = environment.take_run_commands();
assert_eq!(commands.len(), 1);
let (args, _) = &commands[0];
assert_eq!(args.len(), 2);
assert_eq!(args[0], "vi");
assert_eq!(args[1], "/dprint.json");
}
#[test]
fn config_edit_should_prioritize_dprint_editor_over_others() {
let environment = TestEnvironmentBuilder::new()
.with_default_config(|config| {
config.add_includes("**/*.txt");
})
.build();
environment.set_env_var("DPRINT_EDITOR", Some("vim"));
environment.set_env_var("VISUAL", Some("emacs"));
environment.set_env_var("EDITOR", Some("vi"));
environment.set_run_command_result(Ok(Some(0)));
run_test_cli(vec!["config", "edit"], &environment).unwrap();
let commands = environment.take_run_commands();
let (args, _) = &commands[0];
assert_eq!(args[0], "vim");
}
#[test]
fn config_edit_should_handle_editor_with_args() {
let environment = TestEnvironmentBuilder::new()
.with_default_config(|config| {
config.add_includes("**/*.txt");
})
.build();
environment.set_env_var("DPRINT_EDITOR", Some("code --wait"));
environment.set_run_command_result(Ok(Some(0)));
run_test_cli(vec!["config", "edit"], &environment).unwrap();
let commands = environment.take_run_commands();
let (args, _) = &commands[0];
assert_eq!(args.len(), 3);
assert_eq!(args[0], "code");
assert_eq!(args[1], "--wait");
assert_eq!(args[2], "/dprint.json");
}
#[test]
fn config_edit_should_open_global_config() {
let environment = TestEnvironmentBuilder::new().write_file("/config/dprint/dprint.json", "{}").build();
environment.set_run_command_result(Ok(Some(0)));
run_test_cli(vec!["config", "edit", "--global"], &environment).unwrap();
let commands = environment.take_run_commands();
let (args, _) = &commands[0];
assert_eq!(
args[args.len() - 1].to_string_lossy(),
Path::new("/config/dprint").join("dprint.json").to_string_lossy()
);
}
#[test]
fn config_edit_should_error_when_no_config_found() {
let environment = TestEnvironment::new();
let error = run_test_cli(vec!["config", "edit"], &environment).err().unwrap();
assert_eq!(error.to_string(), "Could not find a configuration file. Create one with `dprint init`");
}
#[test]
fn config_edit_should_error_when_no_global_config_found() {
let environment = TestEnvironment::new();
let error = run_test_cli(vec!["config", "edit", "--global"], &environment).err().unwrap();
assert_eq!(
error.to_string(),
"Could not find global dprint.json file. Create one with `dprint init --global`"
);
}
#[test]
fn config_edit_should_error_on_remote_config() {
let environment = TestEnvironmentBuilder::new()
.with_remote_config("https://example.com/dprint.json", |config| {
config.add_includes("**/*.txt");
})
.build();
let error = run_test_cli(vec!["config", "edit", "-c", "https://example.com/dprint.json"], &environment)
.err()
.unwrap();
assert_eq!(error.to_string(), "Cannot edit a remote configuration file 'https://example.com/dprint.json'");
}
#[test]
fn config_edit_should_error_when_editor_exits_with_non_zero() {
let environment = TestEnvironmentBuilder::new()
.with_default_config(|config| {
config.add_includes("**/*.txt");
})
.build();
environment.set_run_command_result(Ok(Some(1)));
let error = run_test_cli(vec!["config", "edit"], &environment).err().unwrap();
assert_eq!(error.to_string(), "Editor exited with code: 1");
}
#[test]
fn config_edit_should_work_with_custom_config_path() {
let environment = TestEnvironmentBuilder::new()
.with_local_config("custom.config.json", |config| {
config.add_includes("**/*.txt");
})
.build();
environment.set_run_command_result(Ok(Some(0)));
run_test_cli(vec!["config", "edit", "-c", "custom.config.json"], &environment).unwrap();
let commands = environment.take_run_commands();
let (args, _) = &commands[0];
assert_eq!(args[args.len() - 1], "/custom.config.json");
}
fn test_plugin_resolver(environment: &TestEnvironment) -> std::rc::Rc<crate::plugins::PluginResolver<TestEnvironment>> {
let plugin_cache = crate::plugins::PluginCache::new(environment.clone());
std::rc::Rc::new(crate::plugins::PluginResolver::new(environment.clone(), plugin_cache))
}
fn process_plugin_npm_tarball() -> Vec<u8> {
use crate::test_helpers::PROCESS_PLUGIN_ZIP_BYTES;
use crate::test_helpers::create_test_npm_tarball;
let zip: &[u8] = &PROCESS_PLUGIN_ZIP_BYTES;
let zip_checksum = crate::utils::get_sha256_checksum(zip);
let plugin_json = format!(
r#"{{
"schemaVersion": 2,
"name": "test-process-plugin",
"version": "0.1.0",
"linux-x86_64": {{ "reference": "./bin.zip", "checksum": "{0}" }},
"linux-aarch64": {{ "reference": "./bin.zip", "checksum": "{0}" }},
"darwin-x86_64": {{ "reference": "./bin.zip", "checksum": "{0}" }},
"darwin-aarch64": {{ "reference": "./bin.zip", "checksum": "{0}" }},
"windows-x86_64": {{ "reference": "./bin.zip", "checksum": "{0}" }},
"windows-aarch64": {{ "reference": "./bin.zip", "checksum": "{0}" }}
}}"#,
zip_checksum
);
create_test_npm_tarball(&[("package/plugin.json", plugin_json.as_bytes()), ("package/bin.zip", zip)])
}
async fn call_resolve_npm_plugin_to_add(
text: &str,
config_path: &CanonicalizedPathBuf,
no_version: bool,
update_package_json: bool,
environment: &TestEnvironment,
) -> Result<super::ResolvedNpmPluginAdd> {
call_resolve_npm_plugin_to_add_checksum(text, config_path, no_version, update_package_json, false, environment).await
}
async fn call_resolve_npm_plugin_to_add_checksum(
text: &str,
config_path: &CanonicalizedPathBuf,
no_version: bool,
update_package_json: bool,
checksum: bool,
environment: &TestEnvironment,
) -> Result<super::ResolvedNpmPluginAdd> {
let plugin_resolver = test_plugin_resolver(environment);
let result = super::resolve_npm_plugin_to_add(
super::ResolveNpmPluginOptions {
text,
config_path,
no_version,
update_package_json,
checksum,
},
environment,
&plugin_resolver,
)
.await;
let _ = environment.take_stderr_messages();
let _ = environment.take_stdout_messages();
result
}
#[tokio::test]
async fn npm_add_pinned_wasm_keeps_bare_form() {
use crate::test_helpers::create_test_npm_tarball;
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
let packument = serde_json::json!({
"dist-tags": { "latest": "0.95.15" },
"versions": { "0.95.15": { "dist": { "tarball": "https://registry.npmjs.org/@dprint/typescript/-/typescript-0.95.15.tgz" } } }
});
environment.add_remote_file_bytes("https://registry.npmjs.org/@dprint/typescript", packument.to_string().into_bytes());
environment.add_remote_file_bytes(
"https://registry.npmjs.org/@dprint/typescript/-/typescript-0.95.15.tgz",
create_test_npm_tarball(&[("package/plugin.wasm", crate::test_helpers::WASM_PLUGIN_BYTES)]),
);
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add("npm:@dprint/typescript@0.95.15", &config_path, false, false, &environment)
.await
.unwrap();
assert_eq!(result.url, "npm:@dprint/typescript@0.95.15");
assert!(result.package_json_addition.is_none());
}
#[tokio::test]
async fn npm_add_pinned_with_explicit_path_passes_through() {
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add("npm:@dprint/typescript@0.95.15/plugin.wasm", &config_path, false, false, &environment)
.await
.unwrap();
assert_eq!(result.url, "npm:@dprint/typescript@0.95.15/plugin.wasm");
assert!(result.package_json_addition.is_none());
}
#[tokio::test]
async fn npm_add_checksum_forces_wasm_checksum_for_unversioned() {
use crate::test_helpers::create_test_npm_tarball;
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
let packument = serde_json::json!({
"dist-tags": { "latest": "1.2.3" },
"versions": { "1.2.3": { "dist": { "tarball": "https://registry.npmjs.org/foo/-/foo-1.2.3.tgz" } } }
});
environment.add_remote_file_bytes("https://registry.npmjs.org/foo", packument.to_string().into_bytes());
let tarball_bytes = create_test_npm_tarball(&[("package/plugin.wasm", crate::test_helpers::WASM_PLUGIN_BYTES)]);
let expected = crate::utils::get_sha256_checksum(&tarball_bytes);
environment.add_remote_file_bytes("https://registry.npmjs.org/foo/-/foo-1.2.3.tgz", tarball_bytes);
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add_checksum("npm:foo", &config_path, false, false, true, &environment)
.await
.unwrap();
assert_eq!(result.url, format!("npm:foo@1.2.3@{}", expected));
}
#[tokio::test]
async fn npm_add_checksum_forces_wasm_checksum_for_versioned() {
use crate::test_helpers::create_test_npm_tarball;
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
let packument = serde_json::json!({
"dist-tags": { "latest": "9.9.9" },
"versions": { "1.2.3": { "dist": { "tarball": "https://registry.npmjs.org/foo/-/foo-1.2.3.tgz" } } }
});
environment.add_remote_file_bytes("https://registry.npmjs.org/foo", packument.to_string().into_bytes());
let tarball_bytes = create_test_npm_tarball(&[("package/plugin.wasm", crate::test_helpers::WASM_PLUGIN_BYTES)]);
let expected = crate::utils::get_sha256_checksum(&tarball_bytes);
environment.add_remote_file_bytes("https://registry.npmjs.org/foo/-/foo-1.2.3.tgz", tarball_bytes);
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add_checksum("npm:foo@1.2.3", &config_path, false, false, true, &environment)
.await
.unwrap();
assert_eq!(result.url, format!("npm:foo@1.2.3@{}", expected));
}
#[tokio::test]
async fn npm_add_checksum_explicit_non_root_path_does_not_inspect_package() {
use crate::test_helpers::create_test_npm_tarball;
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
let packument = serde_json::json!({
"dist-tags": { "latest": "1.0.0" },
"versions": { "1.0.0": { "dist": { "tarball": "https://registry.npmjs.org/foo/-/foo-1.0.0.tgz" } } }
});
environment.add_remote_file_bytes("https://registry.npmjs.org/foo", packument.to_string().into_bytes());
let tarball_bytes = create_test_npm_tarball(&[("package/sub/foo.wasm", crate::test_helpers::WASM_PLUGIN_BYTES)]);
let expected = crate::utils::get_sha256_checksum(&tarball_bytes);
environment.add_remote_file_bytes("https://registry.npmjs.org/foo/-/foo-1.0.0.tgz", tarball_bytes);
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add_checksum("npm:foo@1.0.0/sub/foo.wasm", &config_path, false, false, true, &environment)
.await
.unwrap();
assert_eq!(result.url, format!("npm:foo@1.0.0/sub/foo.wasm@{}", expected));
}
#[tokio::test]
async fn npm_add_explicit_path_in_multi_plugin_package_uses_path_kind() {
use crate::test_helpers::create_test_npm_tarball;
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
let packument = serde_json::json!({
"dist-tags": { "latest": "1.0.0" },
"versions": { "1.0.0": { "dist": { "tarball": "https://registry.npmjs.org/plugins/-/plugins-1.0.0.tgz" } } }
});
environment.add_remote_file_bytes("https://registry.npmjs.org/plugins", packument.to_string().into_bytes());
let tarball_bytes = create_test_npm_tarball(&[
("package/typescript.wasm", crate::test_helpers::WASM_PLUGIN_BYTES),
("package/plugin.json", b"{}"),
]);
let expected = crate::utils::get_sha256_checksum(&tarball_bytes);
environment.add_remote_file_bytes("https://registry.npmjs.org/plugins/-/plugins-1.0.0.tgz", tarball_bytes);
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add_checksum("npm:plugins@1.0.0/typescript.wasm", &config_path, false, false, true, &environment)
.await
.unwrap();
assert_eq!(result.url, format!("npm:plugins@1.0.0/typescript.wasm@{}", expected));
}
#[tokio::test]
async fn npm_add_explicit_process_path_after_wasm_add_derives_kind_from_path() {
use crate::test_helpers::create_test_npm_tarball;
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
let packument = serde_json::json!({
"dist-tags": { "latest": "1.0.0" },
"versions": { "1.0.0": { "dist": { "tarball": "https://registry.npmjs.org/plugins/-/plugins-1.0.0.tgz" } } }
});
environment.add_remote_file_bytes("https://registry.npmjs.org/plugins", packument.to_string().into_bytes());
let tarball_bytes = create_test_npm_tarball(&[
("package/typescript.wasm", crate::test_helpers::WASM_PLUGIN_BYTES),
("package/config.json", b"{}"),
]);
let expected = crate::utils::get_sha256_checksum(&tarball_bytes);
environment.add_remote_file_bytes("https://registry.npmjs.org/plugins/-/plugins-1.0.0.tgz", tarball_bytes);
let config_path = environment.canonicalize("/dprint.json").unwrap();
call_resolve_npm_plugin_to_add_checksum("npm:plugins/typescript.wasm", &config_path, false, false, true, &environment)
.await
.unwrap();
let result = call_resolve_npm_plugin_to_add("npm:plugins/config.json", &config_path, false, false, &environment)
.await
.unwrap();
assert_eq!(result.url, format!("npm:plugins@1.0.0/config.json@{}", expected));
}
#[tokio::test]
async fn npm_add_checksum_pins_instead_of_deferring_to_devdep() {
use crate::test_helpers::create_test_npm_tarball;
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
environment.write_file("/package.json", r#"{"devDependencies": {"foo": "^1.0.0"}}"#).unwrap();
let packument = serde_json::json!({
"dist-tags": { "latest": "1.2.3" },
"versions": { "1.2.3": { "dist": { "tarball": "https://registry.npmjs.org/foo/-/foo-1.2.3.tgz" } } }
});
environment.add_remote_file_bytes("https://registry.npmjs.org/foo", packument.to_string().into_bytes());
let tarball_bytes = create_test_npm_tarball(&[("package/plugin.wasm", crate::test_helpers::WASM_PLUGIN_BYTES)]);
let expected = crate::utils::get_sha256_checksum(&tarball_bytes);
environment.add_remote_file_bytes("https://registry.npmjs.org/foo/-/foo-1.2.3.tgz", tarball_bytes);
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add_checksum("npm:foo", &config_path, false, false, true, &environment)
.await
.unwrap();
assert_eq!(result.url, format!("npm:foo@1.2.3@{}", expected));
}
#[tokio::test]
async fn ensure_url_checksum_appends_for_plugin_url_without_one() {
let environment = TestEnvironment::new();
environment.set_cpu_arch("aarch64");
environment.add_remote_file("https://plugins.dprint.dev/test.wasm", crate::test_helpers::WASM_PLUGIN_BYTES);
let expected = crate::utils::get_sha256_checksum(crate::test_helpers::WASM_PLUGIN_BYTES);
let resolver = test_plugin_resolver(&environment);
let url = super::ensure_url_checksum("https://plugins.dprint.dev/test.wasm".to_string(), &resolver)
.await
.unwrap();
assert_eq!(url, format!("https://plugins.dprint.dev/test.wasm@{}", expected));
let _ = environment.take_stderr_messages();
}
#[tokio::test]
async fn ensure_url_checksum_preserves_existing_and_ignores_non_plugin_urls() {
let environment = TestEnvironment::new();
let resolver = test_plugin_resolver(&environment);
let url = super::ensure_url_checksum("https://example.com/plugin.wasm@abc123".to_string(), &resolver)
.await
.unwrap();
assert_eq!(url, "https://example.com/plugin.wasm@abc123");
let url = super::ensure_url_checksum("https://example.com/readme.txt".to_string(), &resolver)
.await
.unwrap();
assert_eq!(url, "https://example.com/readme.txt");
}
#[tokio::test]
async fn npm_add_defers_to_devdep_when_present() {
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
environment
.write_file("/package.json", r#"{"devDependencies": {"@dprint/typescript": "^0.95.0"}}"#)
.unwrap();
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add("npm:@dprint/typescript", &config_path, false, false, &environment)
.await
.unwrap();
assert_eq!(result.url, "npm:@dprint/typescript");
let _ = environment.take_stderr_messages();
}
#[tokio::test]
async fn npm_add_defers_to_regular_dependency_when_present() {
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
environment
.write_file("/package.json", r#"{"dependencies": {"@dprint/typescript": "^0.95.0"}}"#)
.unwrap();
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add("npm:@dprint/typescript", &config_path, false, false, &environment)
.await
.unwrap();
assert_eq!(result.url, "npm:@dprint/typescript");
let _ = environment.take_stderr_messages();
}
#[tokio::test]
async fn npm_add_defers_to_dep_listed_in_a_parent_package_json() {
let environment = TestEnvironment::new();
environment.mk_dir_all("/repo/packages/web").unwrap();
environment.write_file("/repo/packages/web/dprint.json", "{}").unwrap();
environment.write_file("/repo/packages/web/package.json", r#"{"name": "web"}"#).unwrap();
environment
.write_file("/repo/package.json", r#"{"devDependencies": {"@dprint/typescript": "^0.95.0"}}"#)
.unwrap();
let config_path = environment.canonicalize("/repo/packages/web/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add("npm:@dprint/typescript", &config_path, false, false, &environment)
.await
.unwrap();
assert_eq!(result.url, "npm:@dprint/typescript");
let _ = environment.take_stderr_messages();
}
#[tokio::test]
async fn npm_add_resolves_latest_without_devdep() {
use crate::test_helpers::create_test_npm_tarball;
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
let packument = serde_json::json!({
"dist-tags": { "latest": "1.2.3" },
"versions": { "1.2.3": { "dist": { "tarball": "https://registry.npmjs.org/foo/-/foo-1.2.3.tgz" } } }
});
environment.add_remote_file_bytes("https://registry.npmjs.org/foo", packument.to_string().into_bytes());
let tarball_bytes = create_test_npm_tarball(&[("package/plugin.wasm", crate::test_helpers::WASM_PLUGIN_BYTES)]);
environment.add_remote_file_bytes("https://registry.npmjs.org/foo/-/foo-1.2.3.tgz", tarball_bytes);
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add("npm:foo", &config_path, false, false, &environment)
.await
.unwrap();
assert_eq!(result.url, "npm:foo@1.2.3");
}
#[tokio::test]
async fn npm_add_resolves_latest_with_checksum_for_process_plugin() {
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
let packument = serde_json::json!({
"dist-tags": { "latest": "2.0.0" },
"versions": { "2.0.0": { "dist": { "tarball": "https://registry.npmjs.org/foo/-/foo-2.0.0.tgz" } } }
});
environment.add_remote_file_bytes("https://registry.npmjs.org/foo", packument.to_string().into_bytes());
let tarball_bytes = process_plugin_npm_tarball();
let expected = crate::utils::get_sha256_checksum(&tarball_bytes);
environment.add_remote_file_bytes("https://registry.npmjs.org/foo/-/foo-2.0.0.tgz", tarball_bytes);
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add("npm:foo/plugin.json", &config_path, false, false, &environment)
.await
.unwrap();
assert_eq!(result.url, format!("npm:foo@2.0.0/plugin.json@{}", expected));
}
#[tokio::test]
async fn npm_add_autodetects_process_plugin_without_path() {
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
let packument = serde_json::json!({
"dist-tags": { "latest": "2.0.0" },
"versions": { "2.0.0": { "dist": { "tarball": "https://registry.npmjs.org/foo/-/foo-2.0.0.tgz" } } }
});
environment.add_remote_file_bytes("https://registry.npmjs.org/foo", packument.to_string().into_bytes());
let tarball_bytes = process_plugin_npm_tarball();
let expected = crate::utils::get_sha256_checksum(&tarball_bytes);
environment.add_remote_file_bytes("https://registry.npmjs.org/foo/-/foo-2.0.0.tgz", tarball_bytes);
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add("npm:foo", &config_path, false, false, &environment)
.await
.unwrap();
assert_eq!(result.url, format!("npm:foo@2.0.0/plugin.json@{}", expected));
}
#[tokio::test]
async fn npm_add_autodetects_wasm_plugin_without_path() {
use crate::test_helpers::create_test_npm_tarball;
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
let packument = serde_json::json!({
"dist-tags": { "latest": "2.0.0" },
"versions": { "2.0.0": { "dist": { "tarball": "https://registry.npmjs.org/foo/-/foo-2.0.0.tgz" } } }
});
environment.add_remote_file_bytes("https://registry.npmjs.org/foo", packument.to_string().into_bytes());
let tarball_bytes = create_test_npm_tarball(&[("package/plugin.wasm", crate::test_helpers::WASM_PLUGIN_BYTES)]);
environment.add_remote_file_bytes("https://registry.npmjs.org/foo/-/foo-2.0.0.tgz", tarball_bytes);
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add("npm:foo", &config_path, false, false, &environment)
.await
.unwrap();
assert_eq!(result.url, "npm:foo@2.0.0");
}
#[tokio::test]
async fn npm_add_autodetects_process_plugin_for_versioned_without_path() {
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
let packument = serde_json::json!({
"dist-tags": { "latest": "9.9.9" },
"versions": { "1.2.3": { "dist": { "tarball": "https://registry.npmjs.org/foo/-/foo-1.2.3.tgz" } } }
});
environment.add_remote_file_bytes("https://registry.npmjs.org/foo", packument.to_string().into_bytes());
let tarball_bytes = process_plugin_npm_tarball();
let expected = crate::utils::get_sha256_checksum(&tarball_bytes);
environment.add_remote_file_bytes("https://registry.npmjs.org/foo/-/foo-1.2.3.tgz", tarball_bytes);
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add("npm:foo@1.2.3", &config_path, false, false, &environment)
.await
.unwrap();
assert_eq!(result.url, format!("npm:foo@1.2.3/plugin.json@{}", expected));
}
#[tokio::test]
async fn npm_add_defers_to_devdep_detects_process_path_from_node_modules() {
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
environment.write_file("/package.json", r#"{"devDependencies": {"foo": "^1.0.0"}}"#).unwrap();
environment.mk_dir_all("/node_modules/foo").unwrap();
environment.write_file("/node_modules/foo/plugin.json", "{}").unwrap();
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add("npm:foo", &config_path, false, false, &environment)
.await
.unwrap();
assert_eq!(result.url, "npm:foo/plugin.json");
let _ = environment.take_stderr_messages();
}
#[tokio::test]
async fn npm_add_no_version_detects_process_path_from_node_modules() {
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
environment.mk_dir_all("/node_modules/foo").unwrap();
environment.write_file("/node_modules/foo/plugin.json", "{}").unwrap();
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add("npm:foo", &config_path, true, false, &environment)
.await
.unwrap();
assert_eq!(result.url, "npm:foo/plugin.json");
}
#[tokio::test]
async fn npm_add_no_version_skips_pinning_and_skips_registry() {
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add("npm:@dprint/typescript", &config_path, true, false, &environment)
.await
.unwrap();
assert_eq!(result.url, "npm:@dprint/typescript");
assert!(result.package_json_addition.is_none());
}
#[tokio::test]
async fn npm_add_no_version_errors_on_already_versioned_specifier() {
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
let config_path = environment.canonicalize("/dprint.json").unwrap();
let err = call_resolve_npm_plugin_to_add("npm:@dprint/typescript@1.0.0", &config_path, true, false, &environment)
.await
.unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("--no-version cannot be combined with a versioned specifier"), "got: {msg}");
}
#[tokio::test]
async fn npm_add_package_json_returns_dev_dependency_with_caret_range() {
use crate::test_helpers::create_test_npm_tarball;
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
let packument = serde_json::json!({
"dist-tags": { "latest": "0.99.0" },
"versions": { "0.99.0": { "dist": { "tarball": "https://registry.npmjs.org/@dprint/typescript/-/typescript-0.99.0.tgz" } } }
});
environment.add_remote_file_bytes("https://registry.npmjs.org/@dprint/typescript", packument.to_string().into_bytes());
environment.add_remote_file_bytes(
"https://registry.npmjs.org/@dprint/typescript/-/typescript-0.99.0.tgz",
create_test_npm_tarball(&[("package/plugin.wasm", crate::test_helpers::WASM_PLUGIN_BYTES)]),
);
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add("npm:@dprint/typescript", &config_path, true, true, &environment)
.await
.unwrap();
assert_eq!(result.url, "npm:@dprint/typescript");
assert_eq!(result.package_json_addition, Some(("@dprint/typescript".to_string(), "^0.99.0".to_string())),);
}
#[tokio::test]
async fn npm_add_package_json_detects_process_path_from_node_modules() {
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
environment.mk_dir_all("/node_modules/foo").unwrap();
environment.write_file("/node_modules/foo/plugin.json", "{}").unwrap();
let packument = serde_json::json!({
"dist-tags": { "latest": "1.0.0" },
"versions": { "1.0.0": { "dist": { "tarball": "https://registry.npmjs.org/foo/-/foo-1.0.0.tgz" } } }
});
environment.add_remote_file_bytes("https://registry.npmjs.org/foo", packument.to_string().into_bytes());
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add("npm:foo", &config_path, true, true, &environment).await.unwrap();
assert_eq!(result.url, "npm:foo/plugin.json");
assert_eq!(result.package_json_addition, Some(("foo".to_string(), "^1.0.0".to_string())));
}
#[tokio::test]
async fn apply_package_json_additions_appends_to_devdependencies() {
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
environment.write_file("/package.json", "{\n \"name\": \"app\"\n}\n").unwrap();
let config_path = environment.canonicalize("/dprint.json").unwrap();
super::apply_package_json_additions(&config_path, &[("@dprint/typescript".to_string(), "^0.99.0".to_string())], &environment).unwrap();
let pkg = environment.read_file("/package.json").unwrap();
assert!(pkg.contains("\"devDependencies\""), "got: {pkg}");
assert!(pkg.contains("\"@dprint/typescript\": \"^0.99.0\""), "got: {pkg}");
let _ = environment.take_stderr_messages();
}
#[tokio::test]
async fn apply_package_json_additions_overwrites_existing_entry() {
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
environment
.write_file(
"/package.json",
"{\n \"devDependencies\": {\n \"@dprint/typescript\": \"^0.50.0\"\n }\n}\n",
)
.unwrap();
let config_path = environment.canonicalize("/dprint.json").unwrap();
super::apply_package_json_additions(&config_path, &[("@dprint/typescript".to_string(), "^0.99.0".to_string())], &environment).unwrap();
let pkg = environment.read_file("/package.json").unwrap();
assert!(pkg.contains("\"@dprint/typescript\": \"^0.99.0\""), "got: {pkg}");
assert!(!pkg.contains("\"^0.50.0\""), "old version should be replaced, got: {pkg}");
let _ = environment.take_stderr_messages();
}
#[tokio::test]
async fn apply_package_json_additions_walks_up_to_workspace_root() {
let environment = TestEnvironment::new();
environment.mk_dir_all("/repo/packages/web").unwrap();
environment.write_file("/repo/packages/web/dprint.json", "{}").unwrap();
environment.write_file("/repo/package.json", "{\n \"name\": \"root\"\n}\n").unwrap();
let config_path = environment.canonicalize("/repo/packages/web/dprint.json").unwrap();
super::apply_package_json_additions(&config_path, &[("@dprint/typescript".to_string(), "^0.99.0".to_string())], &environment).unwrap();
let pkg = environment.read_file("/repo/package.json").unwrap();
assert!(pkg.contains("\"@dprint/typescript\": \"^0.99.0\""), "got: {pkg}");
assert!(environment.read_file("/repo/packages/web/package.json").is_err());
let _ = environment.take_stderr_messages();
}
#[tokio::test]
async fn apply_package_json_additions_warns_when_no_package_json_anywhere() {
let environment = TestEnvironment::new();
environment.write_file("/dprint.json", "{}").unwrap();
let config_path = environment.canonicalize("/dprint.json").unwrap();
super::apply_package_json_additions(&config_path, &[("@dprint/typescript".to_string(), "^0.99.0".to_string())], &environment).unwrap();
let stderr = environment.take_stderr_messages();
assert!(
stderr.iter().any(|m| m.contains("no package.json was found") && m.contains("npm init")),
"expected warn, got: {stderr:?}"
);
}
#[test]
fn config_add_npm_replaces_existing_entry_for_same_package() {
use crate::test_helpers::create_test_npm_tarball;
let packument = serde_json::json!({
"dist-tags": { "latest": "2.0.0" },
"versions": { "2.0.0": { "dist": { "tarball": "https://registry.npmjs.org/test-plugin/-/test-plugin-2.0.0.tgz" } } }
});
let environment = TestEnvironmentBuilder::new()
.with_local_config("/dprint.json", |c| {
c.add_plugin("npm:test-plugin@1.0.0");
c.add_plugin("npm:other@1.0.0");
})
.add_remote_file_bytes("https://registry.npmjs.org/test-plugin", packument.to_string().into_bytes())
.add_remote_file_bytes(
"https://registry.npmjs.org/test-plugin/-/test-plugin-2.0.0.tgz",
create_test_npm_tarball(&[("package/plugin.wasm", crate::test_helpers::WASM_PLUGIN_BYTES)]),
)
.build();
run_test_cli(vec!["config", "add", "npm:test-plugin@2.0.0"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("npm:test-plugin@2.0.0"), "new version added, got: {dprint_json}");
assert!(!dprint_json.contains("npm:test-plugin@1.0.0"), "old entry removed, got: {dprint_json}");
assert!(dprint_json.contains("npm:other@1.0.0"), "unrelated entry kept, got: {dprint_json}");
assert_eq!(
dprint_json.matches("npm:test-plugin").count(),
1,
"exactly one entry for the package, got: {dprint_json}"
);
let _ = environment.take_stdout_messages();
let _ = environment.take_stderr_messages();
}
#[test]
fn config_update_skips_unversioned_npm_specifiers() {
use crate::test_helpers::WASM_PLUGIN_BYTES;
let environment = TestEnvironmentBuilder::new()
.with_local_config("/dprint.json", |c| {
c.add_plugin("npm:test-plugin");
})
.write_file("/node_modules/test-plugin/plugin.wasm", WASM_PLUGIN_BYTES)
.build();
run_test_cli(vec!["config", "update"], &environment).unwrap();
let stderr = environment.take_stderr_messages();
assert!(
stderr
.iter()
.any(|m| m.contains("unversioned npm specifier") && m.contains("update via your package manager")),
"expected skip warning, got: {stderr:?}"
);
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("npm:test-plugin"), "got: {dprint_json}");
assert!(!dprint_json.contains("npm:test-plugin@"), "got: {dprint_json}");
}
#[tokio::test]
async fn is_in_package_json_deps_warns_on_malformed_package_json() {
let environment = TestEnvironment::new();
environment.write_file("/package.json", "{ not valid json").unwrap();
let found = super::is_in_package_json_deps("@dprint/typescript", std::path::Path::new("/"), &environment);
assert!(!found);
let stderr = environment.take_stderr_messages();
assert!(
stderr.iter().any(|m| m.contains("/package.json") && m.contains("failed to parse")),
"expected parse warning, got: {stderr:?}"
);
}
}