use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use anyhow::anyhow;
use anyhow::bail;
use deno_semver::Version;
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::paths::get_plugin_names_for_file_on_disk;
use crate::plugins::FetchNpmLatestInfo;
use crate::plugins::InfoFilePluginInfo;
use crate::plugins::MinimumDependencyAgeError;
use crate::plugins::PluginNpmInfo;
use crate::plugins::PluginResolver;
use crate::plugins::PluginSourceReference;
use crate::plugins::PluginUpdateUrlInfo;
use crate::plugins::PluginWrapper;
use crate::plugins::ResolveNpmLatestOptions;
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_dependency_age_cutoff;
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::DependencyAgeCutoff;
use crate::utils::MinimumDependencyAgeArg;
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 minimum_dependency_age: Option<MinimumDependencyAgeArg>,
}
pub async fn init_config_file<TEnvironment: Environment>(
environment: &TEnvironment,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
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)?;
let non_interactive = options.non_interactive || !environment.is_terminal_interactive();
if let Some(existing_path) = config_file_paths.iter().find(|path| environment.path_exists(path)) {
if non_interactive {
bail!("Configuration file '{}' already exists.", existing_path.display())
}
return add_missing_plugins_to_config_file(environment, plugin_resolver, existing_path.clone(), &options).await;
}
let config_file_path = config_file_paths.remove(0);
let config_dir = environment.cwd().join(&config_file_path).parent().map(|dir| dir.to_path_buf());
let text = get_init_config_file_text(
environment,
GetInitConfigFileTextOptions {
non_interactive,
minimum_dependency_age: options.minimum_dependency_age,
config_dir,
},
)
.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(())
}
async fn add_missing_plugins_to_config_file<TEnvironment: Environment>(
environment: &TEnvironment,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
config_file_path: PathBuf,
options: &InitConfigFileOptions<'_>,
) -> Result<()> {
let config_file_path = environment.canonicalize(&config_file_path)?;
let config_dir = config_file_path.parent();
let config = resolve_config_from_path_with_bytes(
&ResolvedConfigPathWithText {
source: PathSource::new_local(config_file_path.clone()),
is_first_download: false,
content: environment.read_file(&config_file_path)?,
base_path: config_dir.clone().unwrap_or_else(|| environment.cwd()),
is_global_config: options.global,
},
environment,
)
.await?;
let existing_plugin_names = get_config_file_plugin_names(environment, plugin_resolver, config.plugins).await;
let plugins_to_add = get_init_plugins_to_add(
environment,
GetInitPluginsToAddOptions {
existing_plugin_names,
minimum_dependency_age: options.minimum_dependency_age.clone(),
config_dir: config_dir.map(|dir| dir.into_path_buf()),
},
)
.await?;
let entries = match plugins_to_add {
InitPluginsToAdd::AllPluginsConfigured => {
log_stdout_info!(environment, "{} already has every plugin dprint knows about.", config_file_path.display());
return Ok(());
}
InitPluginsToAdd::Entries(entries) => entries,
};
if entries.is_empty() {
log_stdout_info!(environment, "\nNo plugins were added.");
return Ok(());
}
let file_text = environment.read_file(&config_file_path)?;
let file_text = add_plugins_to_config(&file_text, &[], &entries)?;
environment.write_file(&config_file_path, &file_text)?;
log_stdout_info!(
environment,
"\nAdded {} to {}",
if entries.len() == 1 {
"1 plugin".to_string()
} else {
format!("{} plugins", entries.len())
},
config_file_path.display()
);
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 minimum_dependency_age: Option<MinimumDependencyAgeArg>,
}
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,
minimum_dependency_age,
} = 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 config_dir = config_path.parent();
let age_cutoff = resolve_dependency_age_cutoff(minimum_dependency_age.as_ref(), config_dir.as_ref().map(|dir| dir.as_ref()), environment);
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.display_name().to_string()).collect::<Vec<_>>(),
)?;
let selected = possible_plugins.remove(index);
let npm_options = ResolveNpmLatestOptions {
force_checksum: checksum,
base_dir: config_dir.clone(),
minimum_dependency_age: age_cutoff.clone(),
};
let url = match selected.resolve_npm(environment, npm_options).await {
Some(resolved) => resolved?.config_file_entry(),
None if checksum => ensure_url_checksum(selected.full_url(), plugin_resolver).await?,
None => 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,
minimum_dependency_age: age_cutoff.as_ref(),
},
&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,
minimum_dependency_age: Option<&'a DependencyAgeCutoff>,
}
#[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,
minimum_dependency_age,
} = options;
if plugin_name_or_url.starts_with("npm:") {
let resolved = resolve_npm_plugin_to_add(
ResolveNpmAddOptions {
text: plugin_name_or_url,
config_path,
no_version,
update_package_json,
checksum,
minimum_dependency_age,
},
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.display_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,
)
}
};
let config_dir = config_path.parent();
let npm_options = |force_checksum: bool| ResolveNpmLatestOptions {
force_checksum,
base_dir: config_dir.clone(),
minimum_dependency_age: minimum_dependency_age.cloned(),
};
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 keep_checksum = checksum || config_plugin_reference.checksum.is_some();
let npm_resolution = plugin.resolve_npm(environment, npm_options(keep_checksum)).await.transpose()?;
if let Some(resolved) = &npm_resolution
&& let PathSource::Npm(existing) = &config_plugin_reference.path_source
&& let Some(existing_version) = existing.specifier.version.as_deref()
&& is_version_downgrade(existing_version, &resolved.version)
{
log_warn!(
environment,
"Skipping {}. The version resolved ({}) is older than the {} in use.",
config_plugin.info().name,
resolved.version,
existing_version,
);
return Ok(None);
}
let (new_version, new_reference) = match npm_resolution {
Some(resolved) => (resolved.version.clone(), resolved.as_source_reference()),
None => (plugin.version.clone(), 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,
new_reference,
},
);
environment.write_file(config_path, &file_text)?;
return Ok(None);
}
}
}
match plugin.resolve_npm(environment, npm_options(checksum)).await.transpose()? {
Some(resolved) => Ok(Some(resolved.config_file_entry())),
None if checksum => Ok(Some(ensure_url_checksum(plugin.full_url(), plugin_resolver).await?)),
None => 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 ResolveNpmAddOptions<'a> {
text: &'a str,
config_path: &'a CanonicalizedPathBuf,
no_version: bool,
update_package_json: bool,
checksum: bool,
minimum_dependency_age: Option<&'a DependencyAgeCutoff>,
}
async fn resolve_npm_plugin_to_add<TEnvironment: Environment>(
options: ResolveNpmAddOptions<'_>,
environment: &TEnvironment,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
) -> Result<ResolvedNpmPluginAdd> {
let ResolveNpmAddOptions {
text,
config_path,
no_version,
update_package_json,
checksum,
minimum_dependency_age,
} = 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,
minimum_dependency_age,
},
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), minimum_dependency_age, 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_plugin_names(environment, plugin_resolver, current_plugins).await;
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 minimum_dependency_age: Option<MinimumDependencyAgeArg>,
}
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,
minimum_dependency_age,
} = 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: None,
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 npm_info_plugins = Rc::new(get_npm_info_plugins(environment).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 config_dir = config_path.parent();
let age_cutoff = resolve_dependency_age_cutoff(minimum_dependency_age.as_ref(), config_dir.as_ref().map(|dir| dir.as_ref()), environment);
let plugins_to_update = get_plugins_to_update(
environment,
plugin_resolver,
config.plugins.clone(),
PluginUpdateContext {
npm_info_plugins: npm_info_plugins.clone(),
config_dir,
age_cutoff,
},
)
.await?;
let mut updated_plugins = Vec::with_capacity(plugins_to_update.len());
for result in plugins_to_update {
match result {
Ok(info) => {
let new_file_text = update_plugin_in_config(&file_text, &info);
if info.is_move_to_npm() && new_file_text == file_text {
log_debug!(
environment,
"Not moving {} to npm — its entry isn't in {} (it likely comes from an extended config).",
info.name,
config_path.display()
);
continue;
}
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 {} {} {}: {}",
info.name,
info.old_version,
if info.is_move_to_npm() { "is moving to npm" } else { "has a new url" },
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())
};
let new_target = if info.is_move_to_npm() {
info.new_reference.display()
} else {
info.new_version.clone()
};
if dry_run {
log_stderr_info!(
environment,
"Would update {} {}{} to {}.",
colors::bold(&info.name),
info.old_version,
in_config,
new_target
);
} else {
log_stderr_info!(environment, "Updating {} {}{} to {}...", info.name, info.old_version, in_config, new_target);
}
if new_file_text != file_text {
file_text = new_file_text;
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) 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,
}
#[derive(Clone)]
struct PluginUpdateContext {
npm_info_plugins: Rc<HashMap<String, InfoFilePluginInfo>>,
config_dir: Option<CanonicalizedPathBuf>,
age_cutoff: Option<DependencyAgeCutoff>,
}
impl PluginUpdateContext {
fn npm_options(&self, force_checksum: bool) -> ResolveNpmLatestOptions {
ResolveNpmLatestOptions {
force_checksum,
base_dir: self.config_dir.clone(),
minimum_dependency_age: self.age_cutoff.clone(),
}
}
}
async fn get_plugins_to_update<TEnvironment: Environment>(
environment: &TEnvironment,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
plugins: Vec<PluginSourceReference>,
context: PluginUpdateContext,
) -> Result<Vec<Result<PluginUpdateInfo, PluginUpdateError>>> {
async fn resolve_plugin_update_info<TEnvironment: Environment>(
environment: &TEnvironment,
plugin_reference: PluginSourceReference,
plugin_result: Result<Rc<PluginWrapper>>,
context: PluginUpdateContext,
) -> 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,
}));
}
};
let old_had_checksum = plugin_reference.checksum.is_some();
if let PathSource::Npm(npm_source) = &plugin_reference.path_source {
let Some(current_version) = npm_source.specifier.version.as_deref() else {
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(),
minimum_dependency_age: context.age_cutoff.as_ref(),
};
match fetch_npm_latest_info(args, environment).await {
Ok(info) if is_version_downgrade(current_version, &info.version) => {
log_warn!(
environment,
"Skipping {}. The version resolved ({}) is older than the {} in use.",
plugin.info().name,
info.version,
current_version,
);
return None;
}
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: current_version.to_string(),
old_reference: plugin_reference,
new_version: info.version,
new_reference,
}));
}
Err(err) => {
if let Some(err) = err.downcast_ref::<MinimumDependencyAgeError>() {
log_warn!(environment, "Skipping {}. {}", plugin.info().name, err);
return None;
}
return Some(Err(PluginUpdateError {
name: plugin_reference.path_source.display(),
error: err,
}));
}
}
}
let latest = read_plugin_latest_info(environment, &plugin).await;
if let Some((npm, plugin_kind)) = npm_package_for_update(&plugin_reference, plugin.info().name.as_str(), latest.as_ref(), &context) {
match npm.resolve_latest(environment, plugin_kind, context.npm_options(old_had_checksum)).await {
Ok(resolved) => {
let new_reference = resolved.as_source_reference();
return Some(Ok(PluginUpdateInfo {
name: plugin.info().name.to_string(),
old_version: plugin.info().version.to_string(),
old_reference: plugin_reference,
new_version: resolved.version,
new_reference,
}));
}
Err(err) if err.downcast_ref::<MinimumDependencyAgeError>().is_some() => {
log_warn!(environment, "Not moving {} to npm yet. {}", plugin.info().name, err)
}
Err(err) => log_warn!(
environment,
"Failed resolving the npm package for {}. Keeping its url. {:#}",
plugin.info().name,
err
),
}
}
let Some(latest) = latest else {
if plugin.info().update_url.is_none() {
log_warn!(
environment,
"Skipping {} as it did not specify an update url. Please update manually.",
plugin.info().name
);
}
return None;
};
match latest.as_source_reference() {
Ok(new_reference) => Some(Ok(PluginUpdateInfo {
name: plugin.info().name.to_string(),
old_reference: plugin_reference,
old_version: plugin.info().version.to_string(),
new_version: latest.version,
new_reference,
})),
Err(err) => {
log_warn!(environment, "Failed reading plugin latest info. {:#}", err);
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();
let context = context.clone();
dprint_core::async_runtime::spawn(async move { resolve_plugin_update_info(&environment, plugin_reference, plugin_result, context).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.is_change()).unwrap_or(true)
{
final_infos.push(info);
}
}
Ok(final_infos)
}
async fn read_plugin_latest_info(environment: &impl Environment, plugin: &PluginWrapper) -> Option<PluginUpdateUrlInfo> {
let update_url = plugin.info().update_url.as_ref()?;
let result = match Url::parse(update_url) {
Ok(update_url) => match read_update_url(environment, &update_url).await {
Ok(Some(info)) => Ok(info),
Ok(None) => Err(anyhow!("Failed downloading {} - 404 Not Found", update_url)),
Err(err) => Err(err),
},
Err(err) => Err(err.into()),
};
match result {
Ok(info) => Some(info),
Err(err) => {
log_warn!(environment, "Failed reading plugin latest info. {:#}", err);
None
}
}
}
fn npm_package_for_update<'a>(
plugin_reference: &PluginSourceReference,
plugin_name: &str,
latest: Option<&'a PluginUpdateUrlInfo>,
context: &'a PluginUpdateContext,
) -> Option<(&'a PluginNpmInfo, PluginKind)> {
let PathSource::Remote(remote_source) = &plugin_reference.path_source else {
return None;
};
if let Some(latest) = latest
&& let Some(npm) = &latest.npm
&& is_same_origin(&remote_source.url, &latest.url)
{
return Some((npm, latest.plugin_kind()));
}
let info_plugin = context.npm_info_plugins.get(plugin_name)?;
let npm = info_plugin.npm.as_ref()?;
is_same_origin(&remote_source.url, &info_plugin.url).then_some((npm, info_plugin.plugin_kind()))
}
async fn get_npm_info_plugins(environment: &impl Environment) -> HashMap<String, InfoFilePluginInfo> {
match read_info_file(environment).await {
Ok(info) => info
.latest_plugins
.into_iter()
.filter(|plugin| plugin.npm.is_some())
.map(|plugin| (plugin.name.clone(), plugin))
.collect(),
Err(err) => {
log_debug!(environment, "Failed reading the plugin info file. {:#}", err);
HashMap::new()
}
}
}
fn is_version_downgrade(old_version: &str, new_version: &str) -> bool {
match (Version::parse_from_npm(old_version), Version::parse_from_npm(new_version)) {
(Ok(old_version), Ok(new_version)) => new_version < old_version,
_ => false,
}
}
fn is_same_origin(config_url: &Url, info_url: &str) -> bool {
match Url::parse(info_url) {
Ok(info_url) => info_url.origin() == config_url.origin(),
Err(_) => false,
}
}
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);
get_plugin_names_for_file_on_disk(&plugins_scope.plugin_name_maps, &file_path, environment)
.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(diagnostics) => bail!("Plugin had {} diagnostic(s)", diagnostics.len()),
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_plugin_names<TEnvironment: Environment>(
environment: &TEnvironment,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
current_plugins: Vec<PluginSourceReference>,
) -> HashSet<String> {
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()
}
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_initialize_with_minimum_dependency_age() {
let mut builder = TestEnvironmentBuilder::new();
let packument = aged_test_plugin_packument("2099-12-31T00:00:00Z");
let environment = add_aged_test_plugin_tarballs(&mut builder)
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.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()),
file_extensions: vec!["ts".to_string()],
npm: Some(crate::environment::TestInfoFileNpm {
name: "@dprint/test-plugin".to_string(),
..Default::default()
}),
..Default::default()
});
})
.write_file("./file.ts", "")
.build();
environment.set_fs_time(AGE_TEST_NOW);
run_test_cli(vec!["init", "--yes", "--minimum-dependency-age", "P3D"], &environment).unwrap();
let created = environment.read_file("./dprint.json").unwrap();
assert!(created.contains("\"npm:@dprint/test-plugin@0.2.0\""), "got: {created}");
let stderr = environment.take_stderr_messages();
assert!(
stderr
.iter()
.any(|m| m.contains("Using @dprint/test-plugin 0.2.0 instead of 0.3.0, which is newer than the minimum dependency age")),
"got: {stderr:?}"
);
environment.take_stdout_messages();
}
#[test]
fn should_initialize_reading_min_release_age_from_the_config_file_directory_npmrc() {
let mut builder = TestEnvironmentBuilder::new();
let packument = aged_test_plugin_packument("2099-12-31T00:00:00Z");
let environment = add_aged_test_plugin_tarballs(&mut builder)
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.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()),
file_extensions: vec!["ts".to_string()],
npm: Some(crate::environment::TestInfoFileNpm {
name: "@dprint/test-plugin".to_string(),
..Default::default()
}),
..Default::default()
});
})
.write_file("./file.ts", "")
.write_file("./.npmrc", "min-release-age=0")
.write_file("/other/.npmrc", "min-release-age=3")
.build();
environment.set_fs_time(AGE_TEST_NOW);
run_test_cli(vec!["init", "--yes", "--config", "/other/dprint.json"], &environment).unwrap();
let created = environment.read_file("/other/dprint.json").unwrap();
assert!(created.contains("\"npm:@dprint/test-plugin@0.2.0\""), "got: {created}");
let stderr = environment.take_stderr_messages();
assert!(
stderr.iter().any(|m| m
.contains("Using @dprint/test-plugin 0.2.0 instead of 0.3.0, which is newer than the minimum dependency age allows (min-release-age=3 in .npmrc). Pass --minimum-dependency-age=0 to use the latest version.")),
"got: {stderr:?}"
);
environment.take_stdout_messages();
}
#[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", "--yes"], &environment).err().unwrap();
assert_eq!(error_message.to_string(), "Configuration file 'dprint.json' already exists.");
environment.set_terminal_interactive(false);
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_add_plugins_to_existing_config_file_on_initialize() {
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.set_multi_selection_result(vec![0]);
run_test_cli(vec!["init"], &environment).unwrap();
assert_eq!(
environment.take_multi_selection_items(),
vec!["[ ] test-process-plugin", "[x] test-plugin — already in config (locked)"]
);
assert_eq!(
environment.take_stderr_messages(),
vec!["Select plugins to add (space to toggle, type to filter, enter to finish):"]
);
assert_eq!(
environment.take_stdout_messages(),
vec![format!("\nAdded 1 plugin to {}", Path::new("/").join("dprint.json").display())]
);
assert_eq!(
environment.read_file("./dprint.json").unwrap(),
r#"{
"plugins": [
"https://plugins.dprint.dev/test-plugin-0.1.0.wasm",
"https://plugins.dprint.dev/test-plugin-3.json"
]
}"#
);
}
#[test]
fn should_not_change_existing_config_file_on_initialize_when_nothing_selected() {
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,
});
let original_text = environment.read_file("./dprint.json").unwrap();
environment.set_multi_selection_result(vec![]);
run_test_cli(vec!["init"], &environment).unwrap();
assert_eq!(
environment.take_stderr_messages(),
vec!["Select plugins to add (space to toggle, type to filter, enter to finish):"]
);
assert_eq!(environment.take_stdout_messages(), vec!["\nNo plugins were added."]);
assert_eq!(environment.read_file("./dprint.json").unwrap(), original_text);
}
#[test]
fn should_not_prompt_on_initialize_when_config_file_has_every_plugin() {
let environment = get_setup_env(SetupEnvOptions {
config_has_wasm: true,
config_has_wasm_checksum: false,
config_has_process: true,
remote_has_wasm_checksum: false,
remote_has_process_checksum: false,
});
run_test_cli(vec!["init"], &environment).unwrap();
assert!(environment.take_multi_selection_items().is_empty());
assert_eq!(
environment.take_stdout_messages(),
vec![format!(
"{} already has every plugin dprint knows about.",
Path::new("/").join("dprint.json").display()
)]
);
}
#[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", "--yes"], &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_resolved_config_for_file_path_with_shebang() {
let environment = TestEnvironmentBuilder::with_initialized_remote_wasm_plugin()
.with_default_config(|c| {
c.add_remote_wasm_plugin().add_config_section(
"shebangs",
r##"{
"#!/bin/sh": "txt"
}"##,
);
})
.write_file(
"/scripts/build",
"#!/bin/sh
text",
)
.write_file("/scripts/notes", "text")
.build();
run_test_cli(vec!["resolved-config", "--file", "/scripts/build"], &environment).unwrap();
assert_eq!(
environment.take_stdout_messages(),
vec![concat!(
"{
",
" \"test-plugin\": {
",
" \"ending\": \"formatted\",
",
" \"lineWidth\": 120
",
" }
",
"}",
)]
);
run_test_cli(vec!["resolved-config", "--file", "/scripts/notes"], &environment).unwrap();
assert_eq!(environment.take_stdout_messages(), vec!["{}"]);
run_test_cli(vec!["resolved-config", "--file", "/scripts/missing"], &environment).unwrap();
assert_eq!(environment.take_stdout_messages(), vec!["{}"]);
}
#[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::ResolveNpmAddOptions {
minimum_dependency_age: None,
text,
config_path,
no_version,
update_package_json,
checksum,
},
environment,
&plugin_resolver,
)
.await;
let _ = environment.take_stderr_messages();
let _ = environment.take_stdout_messages();
result
}
const AGE_TEST_NOW: u64 = 4102444800;
fn aged_foo_packument() -> serde_json::Value {
json!({
"dist-tags": { "latest": "1.1.0" },
"versions": {
"1.0.0": { "dist": { "tarball": "https://registry.npmjs.org/foo/-/foo-1.0.0.tgz" } },
"1.1.0": { "dist": { "tarball": "https://registry.npmjs.org/foo/-/foo-1.1.0.tgz" } },
},
"time": {
"1.0.0": "2099-01-01T00:00:00Z",
"1.1.0": "2099-12-31T00:00:00Z",
}
})
}
async fn call_resolve_npm_plugin_to_add_age(
text: &str,
config_path: &CanonicalizedPathBuf,
age: &str,
package_json: bool,
environment: &TestEnvironment,
) -> Result<super::ResolvedNpmPluginAdd> {
use std::str::FromStr;
let age = crate::utils::MinimumDependencyAgeArg::from_str(age).unwrap();
let cutoff = crate::plugins::resolve_dependency_age_cutoff(Some(&age), None, environment);
let plugin_resolver = test_plugin_resolver(environment);
let result = super::resolve_npm_plugin_to_add(
super::ResolveNpmAddOptions {
minimum_dependency_age: cutoff.as_ref(),
text,
config_path,
no_version: package_json,
update_package_json: package_json,
checksum: false,
},
environment,
&plugin_resolver,
)
.await;
let _ = environment.take_stderr_messages();
let _ = environment.take_stdout_messages();
result
}
#[tokio::test]
async fn npm_add_minimum_dependency_age_pins_the_newest_version_old_enough() {
use crate::test_helpers::WASM_PLUGIN_BYTES;
use crate::test_helpers::create_test_npm_tarball;
let environment = TestEnvironment::new();
environment.set_fs_time(AGE_TEST_NOW);
environment.write_file("/dprint.json", "{}").unwrap();
environment.add_remote_file_bytes("https://registry.npmjs.org/foo", aged_foo_packument().to_string().into_bytes());
environment.add_remote_file_bytes(
"https://registry.npmjs.org/foo/-/foo-1.0.0.tgz",
create_test_npm_tarball(&[("package/plugin.wasm", WASM_PLUGIN_BYTES)]),
);
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add_age("npm:foo", &config_path, "P3D", false, &environment)
.await
.unwrap();
assert_eq!(result.url, "npm:foo@1.0.0");
}
#[tokio::test]
async fn npm_add_package_json_caret_range_respects_the_minimum_dependency_age() {
let environment = TestEnvironment::new();
environment.set_fs_time(AGE_TEST_NOW);
environment.write_file("/dprint.json", "{}").unwrap();
environment.add_remote_file_bytes("https://registry.npmjs.org/foo", aged_foo_packument().to_string().into_bytes());
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add_age("npm:foo", &config_path, "P3D", true, &environment)
.await
.unwrap();
assert_eq!(result.url, "npm:foo");
assert_eq!(result.package_json_addition, Some(("foo".to_string(), "^1.0.0".to_string())));
}
#[tokio::test]
async fn npm_add_package_json_errors_when_nothing_is_old_enough() {
let environment = TestEnvironment::new();
environment.set_fs_time(AGE_TEST_NOW);
environment.write_file("/dprint.json", "{}").unwrap();
environment.add_remote_file_bytes("https://registry.npmjs.org/foo", aged_foo_packument().to_string().into_bytes());
let config_path = environment.canonicalize("/dprint.json").unwrap();
let err = call_resolve_npm_plugin_to_add_age("npm:foo", &config_path, "P400D", true, &environment)
.await
.unwrap_err();
let message = format!("{err:#}");
assert!(message.contains("Resolving latest version for package.json entry of foo"), "got: {message}");
assert!(
message.contains("No version of foo at or below 1.1.0 is old enough for the minimum dependency age"),
"got: {message}"
);
}
#[tokio::test]
async fn npm_add_minimum_dependency_age_leaves_an_explicit_version_alone() {
use crate::test_helpers::WASM_PLUGIN_BYTES;
use crate::test_helpers::create_test_npm_tarball;
let environment = TestEnvironment::new();
environment.set_fs_time(AGE_TEST_NOW);
environment.write_file("/dprint.json", "{}").unwrap();
environment.add_remote_file_bytes("https://registry.npmjs.org/foo", aged_foo_packument().to_string().into_bytes());
environment.add_remote_file_bytes(
"https://registry.npmjs.org/foo/-/foo-1.1.0.tgz",
create_test_npm_tarball(&[("package/plugin.wasm", WASM_PLUGIN_BYTES)]),
);
let config_path = environment.canonicalize("/dprint.json").unwrap();
let result = call_resolve_npm_plugin_to_add_age("npm:foo@1.1.0", &config_path, "P3D", false, &environment)
.await
.unwrap();
assert_eq!(result.url, "npm:foo@1.1.0");
}
#[tokio::test]
async fn npm_add_minimum_dependency_age_errors_when_nothing_is_old_enough() {
let environment = TestEnvironment::new();
environment.set_fs_time(AGE_TEST_NOW);
environment.write_file("/dprint.json", "{}").unwrap();
environment.add_remote_file_bytes("https://registry.npmjs.org/foo", aged_foo_packument().to_string().into_bytes());
let config_path = environment.canonicalize("/dprint.json").unwrap();
let err = call_resolve_npm_plugin_to_add_age("npm:foo", &config_path, "P400D", false, &environment)
.await
.unwrap_err();
assert_eq!(
err.to_string(),
"No version of foo at or below 1.1.0 is old enough for the minimum dependency age (--minimum-dependency-age P400D)."
);
}
#[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_specifier_pins_the_newest_version_old_enough() {
let mut builder = TestEnvironmentBuilder::new();
let packument = aged_test_plugin_packument("2099-12-31T00:00:00Z");
let environment = add_aged_test_plugin_tarballs(&mut builder)
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.with_local_config("/dprint.json", |config| {
config.ensure_plugins_section();
})
.build();
environment.set_fs_time(AGE_TEST_NOW);
run_test_cli(
vec!["config", "add", "npm:@dprint/test-plugin", "--minimum-dependency-age", "P3D"],
&environment,
)
.unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("\"npm:@dprint/test-plugin@0.2.0\""), "got: {dprint_json}");
let _ = environment.take_stdout_messages();
let _ = environment.take_stderr_messages();
}
#[test]
fn config_add_npm_specifier_reads_min_release_age_from_an_npmrc() {
let mut builder = TestEnvironmentBuilder::new();
let packument = aged_test_plugin_packument("2099-12-31T00:00:00Z");
let environment = add_aged_test_plugin_tarballs(&mut builder)
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.with_local_config("/dprint.json", |config| {
config.ensure_plugins_section();
})
.write_file("/.npmrc", "min-release-age=3")
.build();
environment.set_fs_time(AGE_TEST_NOW);
run_test_cli(vec!["config", "add", "npm:@dprint/test-plugin"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("\"npm:@dprint/test-plugin@0.2.0\""), "got: {dprint_json}");
let stderr = environment.take_stderr_messages();
assert!(
stderr.iter().any(|m| m
.contains("Using @dprint/test-plugin 0.2.0 instead of 0.3.0, which is newer than the minimum dependency age allows (min-release-age=3 in .npmrc). Pass --minimum-dependency-age=0 to use the latest version.")),
"got: {stderr:?}"
);
let _ = environment.take_stdout_messages();
}
#[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}");
}
fn add_npm_test_plugin_package<'a>(builder: &'a mut TestEnvironmentBuilder, npm_version: &str) -> &'a mut TestEnvironmentBuilder {
use crate::test_helpers::WASM_PLUGIN_BYTES;
use crate::test_helpers::create_test_npm_tarball;
let tarball_url = format!("https://registry.npmjs.org/@dprint/test-plugin/-/test-plugin-{}.tgz", npm_version);
let packument = json!({
"dist-tags": { "latest": npm_version },
"versions": { npm_version: { "dist": { "tarball": tarball_url } } }
});
builder
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.add_remote_file_bytes(&tarball_url, create_test_npm_tarball(&[("package/plugin.wasm", WASM_PLUGIN_BYTES)]))
}
fn npm_test_plugin_tarball_checksum() -> String {
crate::utils::get_sha256_checksum(&crate::test_helpers::create_test_npm_tarball(&[(
"package/plugin.wasm",
crate::test_helpers::WASM_PLUGIN_BYTES,
)]))
}
fn test_plugin_latest_json(npm: bool) -> String {
let mut value = json!({
"schemaVersion": 1,
"url": "https://plugins.dprint.dev/test-plugin.wasm",
"version": "0.2.0",
});
if npm {
value["npm"] = json!({ "name": "@dprint/test-plugin" });
}
value.to_string()
}
fn npm_info_file_builder(config_plugin_urls: &[&str]) -> TestEnvironmentBuilder {
let mut builder = TestEnvironmentBuilder::new();
add_npm_test_plugin_package(&mut builder, "0.3.0")
.add_remote_wasm_plugin()
.add_remote_wasm_0_1_0_plugin()
.add_remote_file("https://plugins.dprint.dev/dprint/test-plugin/latest.json", &test_plugin_latest_json(false))
.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()),
npm: Some(crate::environment::TestInfoFileNpm {
name: "@dprint/test-plugin".to_string(),
..Default::default()
}),
..Default::default()
});
})
.with_local_config("/dprint.json", |config| {
config.ensure_plugins_section();
for url in config_plugin_urls {
config.add_plugin(url);
}
});
builder
}
#[test]
fn config_add_writes_npm_specifier_from_info_file() {
let environment = npm_info_file_builder(&[]).initialize().build();
environment.set_selection_result(0);
run_test_cli(vec!["config", "add"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("\"npm:@dprint/test-plugin@0.3.0\""), "got: {dprint_json}");
assert_eq!(environment.take_stderr_messages(), vec!["Select a plugin to add:"]);
}
#[test]
fn config_add_checksum_uses_the_npm_packages_checksum() {
let environment = npm_info_file_builder(&[]).initialize().build();
environment.set_selection_result(0);
run_test_cli(vec!["config", "add", "--checksum"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(
dprint_json.contains(&format!("\"npm:@dprint/test-plugin@0.3.0@{}\"", npm_test_plugin_tarball_checksum())),
"got: {dprint_json}"
);
assert_eq!(environment.take_stderr_messages(), vec!["Select a plugin to add:"]);
}
#[test]
fn config_add_errors_when_the_npm_registry_is_unreachable() {
let environment = TestEnvironmentBuilder::new()
.add_remote_wasm_plugin()
.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()),
npm: Some(crate::environment::TestInfoFileNpm {
name: "@dprint/test-plugin".to_string(),
..Default::default()
}),
..Default::default()
});
})
.with_local_config("/dprint.json", |config| {
config.ensure_plugins_section();
})
.initialize()
.build();
environment.set_selection_result(0);
let err = run_test_cli(vec!["config", "add"], &environment).err().unwrap();
assert_contains!(err.to_string(), "Failed to fetch npm packument for @dprint/test-plugin");
let _ = environment.take_stderr_messages();
}
#[test]
fn config_add_by_name_writes_npm_specifier_from_latest_json() {
let mut builder = TestEnvironmentBuilder::new();
let environment = add_npm_test_plugin_package(&mut builder, "0.3.0")
.add_remote_wasm_plugin()
.with_info_file(|_| {})
.with_local_config("/dprint.json", |config| {
config.ensure_plugins_section();
})
.add_remote_file("https://plugins.dprint.dev/dprint/test-plugin/latest.json", &test_plugin_latest_json(true))
.initialize()
.build();
run_test_cli(vec!["config", "add", "test-plugin"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("\"npm:@dprint/test-plugin@0.3.0\""), "got: {dprint_json}");
assert_eq!(environment.take_stderr_messages(), Vec::<String>::new());
}
#[test]
fn config_add_by_name_moves_existing_url_entry_to_npm() {
let mut builder = get_setup_builder(SetupEnvOptions {
config_has_wasm: true,
config_has_wasm_checksum: true,
config_has_process: false,
remote_has_wasm_checksum: false,
remote_has_process_checksum: false,
});
let environment = add_npm_test_plugin_package(&mut builder, "0.3.0")
.add_remote_file("https://plugins.dprint.dev/dprint/test-plugin/latest.json", &test_plugin_latest_json(true))
.initialize()
.build();
run_test_cli(vec!["config", "add", "test-plugin"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(
dprint_json.contains(&format!("\"npm:@dprint/test-plugin@0.3.0@{}\"", npm_test_plugin_tarball_checksum())),
"got: {dprint_json}"
);
assert!(!dprint_json.contains("test-plugin-0.1.0.wasm"), "got: {dprint_json}");
let _ = environment.take_stderr_messages();
}
#[test]
fn config_add_by_name_does_not_downgrade_an_existing_npm_entry() {
let mut builder = TestEnvironmentBuilder::new();
let packument = aged_test_plugin_packument("2099-12-31T00:00:00Z");
let environment = add_aged_test_plugin_tarballs(&mut builder)
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.with_info_file(|info| {
info.add_plugin(TestInfoFilePlugin {
name: "test-plugin".to_string(),
version: "0.3.0".to_string(),
url: "https://plugins.dprint.dev/test-plugin.wasm".to_string(),
config_key: Some("test-plugin".to_string()),
file_extensions: vec!["ts".to_string()],
npm: Some(crate::environment::TestInfoFileNpm {
name: "@dprint/test-plugin".to_string(),
..Default::default()
}),
..Default::default()
});
})
.add_remote_file("https://plugins.dprint.dev/dprint/test-plugin/latest.json", &test_plugin_latest_json(true))
.with_local_config("/dprint.json", |config| {
config.add_plugin("npm:@dprint/test-plugin@0.3.0");
})
.initialize()
.build();
environment.set_fs_time(AGE_TEST_NOW);
let before = environment.read_file("/dprint.json").unwrap();
run_test_cli(vec!["config", "add", "test-plugin", "--minimum-dependency-age", "P3D"], &environment).unwrap();
assert_eq!(environment.read_file("/dprint.json").unwrap(), before);
let stderr = environment.take_stderr_messages();
assert!(
stderr
.iter()
.any(|m| m.contains("Skipping test-plugin. The version resolved (0.2.0) is older than the 0.3.0 in use.")),
"got: {stderr:?}"
);
let _ = environment.take_stdout_messages();
}
#[test]
fn config_update_moves_url_plugin_to_npm() {
let environment = npm_info_file_builder(&["https://plugins.dprint.dev/test-plugin-0.1.0.wasm"])
.initialize()
.build();
run_test_cli(vec!["config", "update"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("\"npm:@dprint/test-plugin@0.3.0\""), "got: {dprint_json}");
assert!(!dprint_json.contains("plugins.dprint.dev"), "got: {dprint_json}");
assert_eq!(
environment.take_stderr_messages(),
vec![
"Updating test-plugin 0.1.0 to npm:@dprint/test-plugin@0.3.0...",
"Compiling /cache/npm/registry.npmjs.org/@dprint__test-plugin@0.3.0/plugin.wasm",
]
);
}
#[test]
fn config_update_moves_url_plugin_to_npm_from_latest_json() {
let mut builder = TestEnvironmentBuilder::new();
let environment = add_npm_test_plugin_package(&mut builder, "0.3.0")
.add_remote_wasm_0_1_0_plugin()
.with_info_file(|_| {})
.add_remote_file("https://plugins.dprint.dev/dprint/test-plugin/latest.json", &test_plugin_latest_json(true))
.with_local_config("/dprint.json", |config| {
config.add_plugin("https://plugins.dprint.dev/test-plugin-0.1.0.wasm");
})
.initialize()
.build();
run_test_cli(vec!["config", "update"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("\"npm:@dprint/test-plugin@0.3.0\""), "got: {dprint_json}");
assert_eq!(
environment.take_stderr_messages(),
vec![
"Updating test-plugin 0.1.0 to npm:@dprint/test-plugin@0.3.0...",
"Compiling /cache/npm/registry.npmjs.org/@dprint__test-plugin@0.3.0/plugin.wasm",
]
);
}
#[test]
fn config_update_keeps_a_pinned_checksum_when_moving_to_npm() {
let old_entry = format!(
"https://plugins.dprint.dev/test-plugin-0.1.0.wasm@{}",
crate::utils::get_sha256_checksum(crate::test_helpers::WASM_PLUGIN_0_1_0_BYTES)
);
let environment = npm_info_file_builder(&[&old_entry]).initialize().build();
run_test_cli(vec!["config", "update"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(
dprint_json.contains(&format!("\"npm:@dprint/test-plugin@0.3.0@{}\"", npm_test_plugin_tarball_checksum())),
"got: {dprint_json}"
);
let _ = environment.take_stderr_messages();
}
#[test]
fn config_update_moves_url_plugin_to_npm_at_same_version() {
let mut builder = TestEnvironmentBuilder::new();
let environment = add_npm_test_plugin_package(&mut builder, "0.2.0")
.add_remote_wasm_plugin()
.add_remote_file("https://plugins.dprint.dev/dprint/test-plugin/latest.json", &test_plugin_latest_json(true))
.with_info_file(|_| {})
.with_local_config("/dprint.json", |config| {
config.add_plugin("https://plugins.dprint.dev/test-plugin.wasm");
})
.initialize()
.build();
run_test_cli(vec!["config", "update"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("\"npm:@dprint/test-plugin@0.2.0\""), "got: {dprint_json}");
assert_eq!(
environment.take_stderr_messages(),
vec![
"Updating test-plugin 0.2.0 to npm:@dprint/test-plugin@0.2.0...",
"Compiling /cache/npm/registry.npmjs.org/@dprint__test-plugin@0.2.0/plugin.wasm",
]
);
}
#[test]
fn config_update_dry_run_previews_an_npm_move() {
let environment = npm_info_file_builder(&["https://plugins.dprint.dev/test-plugin-0.1.0.wasm"])
.initialize()
.build();
let before = environment.read_file("/dprint.json").unwrap();
run_test_cli(vec!["config", "update", "--dry-run"], &environment).unwrap();
assert_eq!(environment.read_file("/dprint.json").unwrap(), before);
let stdout = environment.take_stdout_messages().join("\n");
assert_contains!(stdout, "npm:@dprint/test-plugin@0.3.0");
let stderr = environment.take_stderr_messages();
assert!(
stderr
.iter()
.any(|m| m.contains("Would update") && m.contains("0.1.0 to npm:@dprint/test-plugin@0.3.0.")),
"got: {stderr:?}"
);
}
#[test]
fn config_update_keeps_the_url_when_the_npm_registry_is_unreachable() {
let environment = TestEnvironmentBuilder::new()
.add_remote_wasm_plugin()
.add_remote_wasm_0_1_0_plugin()
.with_info_file(|_| {})
.add_remote_file("https://plugins.dprint.dev/dprint/test-plugin/latest.json", &test_plugin_latest_json(true))
.with_local_config("/dprint.json", |config| {
config.add_plugin("https://plugins.dprint.dev/test-plugin-0.1.0.wasm");
})
.initialize()
.build();
run_test_cli(vec!["config", "update"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("\"https://plugins.dprint.dev/test-plugin.wasm\""), "got: {dprint_json}");
let stderr = environment.take_stderr_messages();
assert!(
stderr
.iter()
.any(|m| m.contains("Failed resolving the npm package for test-plugin. Keeping its url.")),
"got: {stderr:?}"
);
assert!(stderr.contains(&"Updating test-plugin 0.1.0 to 0.2.0...".to_string()), "got: {stderr:?}");
}
#[test]
fn config_update_does_not_move_a_plugin_from_an_extends_ed_config() {
let mut builder = TestEnvironmentBuilder::new();
let environment = add_npm_test_plugin_package(&mut builder, "0.2.0")
.add_remote_wasm_plugin()
.add_remote_file("https://plugins.dprint.dev/dprint/test-plugin/latest.json", &test_plugin_latest_json(true))
.with_info_file(|_| {})
.with_local_config("/base.json", |config| {
config.add_plugin("https://plugins.dprint.dev/test-plugin.wasm");
})
.with_local_config("/dprint.json", |config| {
config.add_config_section("extends", r#""./base.json""#);
})
.initialize()
.build();
let before = environment.read_file("/dprint.json").unwrap();
run_test_cli(vec!["config", "update"], &environment).unwrap();
assert_eq!(environment.read_file("/dprint.json").unwrap(), before);
assert!(
environment
.read_file("/base.json")
.unwrap()
.contains("https://plugins.dprint.dev/test-plugin.wasm"),
"the extended config isn't this run's to rewrite"
);
let stderr = environment.take_stderr_messages();
assert!(!stderr.iter().any(|m| m.contains("Updating")), "got: {stderr:?}");
}
#[test]
fn config_update_moves_to_npm_even_when_the_package_version_is_behind() {
let mut builder = TestEnvironmentBuilder::new();
let environment = add_npm_test_plugin_package(&mut builder, "0.1.0")
.add_remote_wasm_plugin()
.add_remote_file("https://plugins.dprint.dev/dprint/test-plugin/latest.json", &test_plugin_latest_json(true))
.with_info_file(|_| {})
.with_local_config("/dprint.json", |config| {
config.add_plugin("https://plugins.dprint.dev/test-plugin.wasm");
})
.initialize()
.build();
run_test_cli(vec!["config", "update"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("\"npm:@dprint/test-plugin@0.1.0\""), "got: {dprint_json}");
assert_eq!(
environment.take_stderr_messages(),
vec![
"Updating test-plugin 0.2.0 to npm:@dprint/test-plugin@0.1.0...",
"Compiling /cache/npm/registry.npmjs.org/@dprint__test-plugin@0.1.0/plugin.wasm",
]
);
}
#[test]
fn config_update_skips_an_npm_plugin_whose_package_went_backwards() {
use crate::test_helpers::WASM_PLUGIN_BYTES;
use crate::test_helpers::create_test_npm_tarball;
let tarball_url = |version: &str| format!("https://registry.npmjs.org/@dprint/test-plugin/-/test-plugin-{version}.tgz");
let packument = json!({
"dist-tags": { "latest": "0.1.0" },
"versions": {
"0.1.0": { "dist": { "tarball": tarball_url("0.1.0") } },
"0.2.0": { "dist": { "tarball": tarball_url("0.2.0") } },
}
});
let environment = TestEnvironmentBuilder::new()
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.add_remote_file_bytes(&tarball_url("0.2.0"), create_test_npm_tarball(&[("package/plugin.wasm", WASM_PLUGIN_BYTES)]))
.with_info_file(|_| {})
.with_local_config("/dprint.json", |config| {
config.add_plugin("npm:@dprint/test-plugin@0.2.0");
})
.initialize()
.build();
let before = environment.read_file("/dprint.json").unwrap();
run_test_cli(vec!["config", "update"], &environment).unwrap();
assert_eq!(environment.read_file("/dprint.json").unwrap(), before);
let stderr = environment.take_stderr_messages();
assert!(
stderr
.iter()
.any(|m| m.contains("Skipping test-plugin. The version resolved (0.1.0) is older than the 0.2.0 in use.")),
"got: {stderr:?}"
);
}
fn aged_test_plugin_packument(latest_time: &str) -> serde_json::Value {
let tarball_url = |version: &str| format!("https://registry.npmjs.org/@dprint/test-plugin/-/test-plugin-{version}.tgz");
json!({
"dist-tags": { "latest": "0.3.0" },
"versions": {
"0.1.0": { "dist": { "tarball": tarball_url("0.1.0") } },
"0.2.0": { "dist": { "tarball": tarball_url("0.2.0") } },
"0.3.0": { "dist": { "tarball": tarball_url("0.3.0") } },
},
"time": {
"0.1.0": "2098-01-01T00:00:00Z",
"0.2.0": "2099-01-01T00:00:00Z",
"0.3.0": latest_time,
}
})
}
fn add_aged_test_plugin_tarballs(builder: &mut TestEnvironmentBuilder) -> &mut TestEnvironmentBuilder {
use crate::test_helpers::WASM_PLUGIN_BYTES;
use crate::test_helpers::create_test_npm_tarball;
for version in ["0.1.0", "0.2.0", "0.3.0"] {
builder.add_remote_file_bytes(
&format!("https://registry.npmjs.org/@dprint/test-plugin/-/test-plugin-{version}.tgz"),
create_test_npm_tarball(&[("package/plugin.wasm", WASM_PLUGIN_BYTES)]),
);
}
builder
}
#[test]
fn config_update_stops_at_the_newest_version_old_enough() {
let mut builder = TestEnvironmentBuilder::new();
let packument = aged_test_plugin_packument("2099-12-31T00:00:00Z");
let environment = add_aged_test_plugin_tarballs(&mut builder)
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.with_info_file(|_| {})
.with_local_config("/dprint.json", |config| {
config.add_plugin("npm:@dprint/test-plugin@0.1.0");
})
.initialize()
.build();
environment.set_fs_time(AGE_TEST_NOW);
run_test_cli(vec!["config", "update", "--minimum-dependency-age", "P3D"], &environment).unwrap();
assert!(
environment.read_file("/dprint.json").unwrap().contains("npm:@dprint/test-plugin@0.2.0"),
"got: {}",
environment.read_file("/dprint.json").unwrap()
);
let stderr = environment.take_stderr_messages();
assert!(
stderr
.iter()
.any(|m| m.contains("Using @dprint/test-plugin 0.2.0 instead of 0.3.0, which is newer than the minimum dependency age")),
"got: {stderr:?}"
);
}
#[test]
fn config_update_skips_a_plugin_with_no_version_old_enough() {
let mut builder = TestEnvironmentBuilder::new();
let packument = aged_test_plugin_packument("2099-12-31T00:00:00Z");
let environment = add_aged_test_plugin_tarballs(&mut builder)
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.with_info_file(|_| {})
.with_local_config("/dprint.json", |config| {
config.add_plugin("npm:@dprint/test-plugin@0.1.0");
})
.initialize()
.build();
environment.set_fs_time(AGE_TEST_NOW);
let before = environment.read_file("/dprint.json").unwrap();
run_test_cli(vec!["config", "update", "--minimum-dependency-age", "P365000D"], &environment).unwrap();
assert_eq!(environment.read_file("/dprint.json").unwrap(), before);
let stderr = environment.take_stderr_messages();
assert!(
stderr
.iter()
.any(|m| m
.contains("Skipping test-plugin. No version of @dprint/test-plugin at or below 0.3.0 is old enough for the minimum dependency age (--minimum-dependency-age P365000D).")),
"got: {stderr:?}"
);
}
#[test]
fn config_update_is_not_held_back_without_the_flag() {
let mut builder = TestEnvironmentBuilder::new();
let packument = aged_test_plugin_packument("2099-12-31T00:00:00Z");
let environment = add_aged_test_plugin_tarballs(&mut builder)
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.with_info_file(|_| {})
.with_local_config("/dprint.json", |config| {
config.add_plugin("npm:@dprint/test-plugin@0.1.0");
})
.initialize()
.build();
environment.set_fs_time(AGE_TEST_NOW);
run_test_cli(vec!["config", "update"], &environment).unwrap();
assert!(
environment.read_file("/dprint.json").unwrap().contains("npm:@dprint/test-plugin@0.3.0"),
"got: {}",
environment.read_file("/dprint.json").unwrap()
);
environment.take_stderr_messages();
}
#[test]
fn config_update_reads_min_release_age_from_an_npmrc() {
let mut builder = TestEnvironmentBuilder::new();
let packument = aged_test_plugin_packument("2099-12-31T00:00:00Z");
let environment = add_aged_test_plugin_tarballs(&mut builder)
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.with_info_file(|_| {})
.with_local_config("/dprint.json", |config| {
config.add_plugin("npm:@dprint/test-plugin@0.1.0");
})
.initialize()
.build();
environment.set_fs_time(AGE_TEST_NOW);
environment.write_file("/.npmrc", "min-release-age=3").unwrap();
run_test_cli(vec!["config", "update"], &environment).unwrap();
assert!(
environment.read_file("/dprint.json").unwrap().contains("npm:@dprint/test-plugin@0.2.0"),
"got: {}",
environment.read_file("/dprint.json").unwrap()
);
environment.take_stderr_messages();
}
#[test]
fn config_update_reads_min_release_age_from_a_parent_directory_npmrc() {
let mut builder = TestEnvironmentBuilder::new();
let packument = aged_test_plugin_packument("2099-12-31T00:00:00Z");
let environment = add_aged_test_plugin_tarballs(&mut builder)
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.with_info_file(|_| {})
.with_local_config("/sub/dprint.json", |config| {
config.add_plugin("npm:@dprint/test-plugin@0.1.0");
})
.set_cwd("/sub")
.initialize()
.build();
environment.set_fs_time(AGE_TEST_NOW);
environment.write_file("/.npmrc", "min-release-age=3").unwrap();
run_test_cli(vec!["config", "update"], &environment).unwrap();
assert!(
environment.read_file("/sub/dprint.json").unwrap().contains("npm:@dprint/test-plugin@0.2.0"),
"got: {}",
environment.read_file("/sub/dprint.json").unwrap()
);
environment.take_stderr_messages();
}
#[test]
fn config_update_resolves_the_min_release_age_per_config_file() {
let mut builder = TestEnvironmentBuilder::new();
let packument = aged_test_plugin_packument("2099-12-31T00:00:00Z");
let environment = add_aged_test_plugin_tarballs(&mut builder)
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.with_info_file(|_| {})
.with_local_config("/dprint.json", |config| {
config.add_plugin("npm:@dprint/test-plugin@0.1.0");
})
.with_local_config("/sub/dprint.json", |config| {
config.add_plugin("npm:@dprint/test-plugin@0.1.0");
})
.initialize()
.build();
environment.set_fs_time(AGE_TEST_NOW);
environment.write_file("/sub/.npmrc", "min-release-age=3").unwrap();
run_test_cli(vec!["config", "update", "--recursive"], &environment).unwrap();
let root = environment.read_file("/dprint.json").unwrap();
assert!(root.contains("npm:@dprint/test-plugin@0.3.0"), "got: {root}");
let sub = environment.read_file("/sub/dprint.json").unwrap();
assert!(sub.contains("npm:@dprint/test-plugin@0.2.0"), "got: {sub}");
environment.take_stderr_messages();
}
#[test]
fn config_update_flag_turns_off_an_npmrc_min_release_age() {
let mut builder = TestEnvironmentBuilder::new();
let packument = aged_test_plugin_packument("2099-12-31T00:00:00Z");
let environment = add_aged_test_plugin_tarballs(&mut builder)
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.with_info_file(|_| {})
.with_local_config("/dprint.json", |config| {
config.add_plugin("npm:@dprint/test-plugin@0.1.0");
})
.initialize()
.build();
environment.set_fs_time(AGE_TEST_NOW);
environment.write_file("/.npmrc", "min-release-age=3").unwrap();
run_test_cli(vec!["config", "update", "--minimum-dependency-age", "0"], &environment).unwrap();
assert!(
environment.read_file("/dprint.json").unwrap().contains("npm:@dprint/test-plugin@0.3.0"),
"got: {}",
environment.read_file("/dprint.json").unwrap()
);
environment.take_stderr_messages();
}
#[test]
fn config_update_does_nothing_when_the_newest_version_old_enough_is_in_use() {
let mut builder = TestEnvironmentBuilder::new();
let packument = aged_test_plugin_packument("2099-12-31T00:00:00Z");
let environment = add_aged_test_plugin_tarballs(&mut builder)
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.with_info_file(|_| {})
.with_local_config("/dprint.json", |config| {
config.add_plugin("npm:@dprint/test-plugin@0.2.0");
})
.initialize()
.build();
environment.set_fs_time(AGE_TEST_NOW);
let before = environment.read_file("/dprint.json").unwrap();
run_test_cli(vec!["config", "update", "--minimum-dependency-age", "P3D"], &environment).unwrap();
assert_eq!(environment.read_file("/dprint.json").unwrap(), before);
let stderr = environment.take_stderr_messages();
assert!(!stderr.iter().any(|m| m.contains("Updating")), "got: {stderr:?}");
}
#[test]
fn config_update_does_not_downgrade_when_the_version_in_use_is_too_new() {
let mut builder = TestEnvironmentBuilder::new();
let packument = aged_test_plugin_packument("2099-12-31T00:00:00Z");
let environment = add_aged_test_plugin_tarballs(&mut builder)
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.with_info_file(|_| {})
.with_local_config("/dprint.json", |config| {
config.add_plugin("npm:@dprint/test-plugin@0.3.0");
})
.initialize()
.build();
environment.set_fs_time(AGE_TEST_NOW);
let before = environment.read_file("/dprint.json").unwrap();
run_test_cli(vec!["config", "update", "--minimum-dependency-age", "P3D"], &environment).unwrap();
assert_eq!(environment.read_file("/dprint.json").unwrap(), before);
let stderr = environment.take_stderr_messages();
assert!(
stderr
.iter()
.any(|m| m.contains("Skipping test-plugin. The version resolved (0.2.0) is older than the 0.3.0 in use.")),
"got: {stderr:?}"
);
}
#[test]
fn config_update_moves_url_plugin_to_npm_at_an_older_version() {
let mut builder = TestEnvironmentBuilder::new();
let packument = aged_test_plugin_packument("2099-12-31T00:00:00Z");
let environment = add_aged_test_plugin_tarballs(&mut builder)
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.add_remote_wasm_0_1_0_plugin()
.with_info_file(|_| {})
.add_remote_file("https://plugins.dprint.dev/dprint/test-plugin/latest.json", &test_plugin_latest_json(true))
.with_local_config("/dprint.json", |config| {
config.add_plugin("https://plugins.dprint.dev/test-plugin-0.1.0.wasm");
})
.initialize()
.build();
environment.set_fs_time(AGE_TEST_NOW);
run_test_cli(vec!["config", "update", "--minimum-dependency-age", "P3D"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("\"npm:@dprint/test-plugin@0.2.0\""), "got: {dprint_json}");
environment.take_stderr_messages();
}
#[test]
fn config_update_keeps_the_url_when_no_npm_version_is_old_enough() {
let mut builder = TestEnvironmentBuilder::new();
let packument = aged_test_plugin_packument("2099-12-31T00:00:00Z");
let environment = add_aged_test_plugin_tarballs(&mut builder)
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.add_remote_wasm_plugin()
.add_remote_wasm_0_1_0_plugin()
.with_info_file(|_| {})
.add_remote_file("https://plugins.dprint.dev/dprint/test-plugin/latest.json", &test_plugin_latest_json(true))
.with_local_config("/dprint.json", |config| {
config.add_plugin("https://plugins.dprint.dev/test-plugin-0.1.0.wasm");
})
.initialize()
.build();
environment.set_fs_time(AGE_TEST_NOW);
run_test_cli(vec!["config", "update", "--minimum-dependency-age", "P365000D"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("\"https://plugins.dprint.dev/test-plugin.wasm\""), "got: {dprint_json}");
let stderr = environment.take_stderr_messages();
assert!(
stderr.iter().any(|m| m.contains(
"Not moving test-plugin to npm yet. No version of @dprint/test-plugin at or below 0.3.0 is old enough for the minimum dependency age (--minimum-dependency-age P365000D)."
)),
"got: {stderr:?}"
);
}
#[test]
fn config_add_by_name_uses_the_newest_version_old_enough() {
let mut builder = TestEnvironmentBuilder::new();
let packument = aged_test_plugin_packument("2099-12-31T00:00:00Z");
let environment = add_aged_test_plugin_tarballs(&mut builder)
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.add_remote_wasm_plugin()
.with_info_file(|_| {})
.add_remote_file("https://plugins.dprint.dev/dprint/test-plugin/latest.json", &test_plugin_latest_json(true))
.with_local_config("/dprint.json", |config| {
config.ensure_plugins_section();
})
.initialize()
.build();
environment.set_fs_time(AGE_TEST_NOW);
run_test_cli(vec!["config", "add", "test-plugin", "--minimum-dependency-age", "P3D"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("\"npm:@dprint/test-plugin@0.2.0\""), "got: {dprint_json}");
environment.take_stderr_messages();
}
#[test]
fn config_update_tracks_the_packages_version_not_the_plugins_own() {
let mut builder = TestEnvironmentBuilder::new();
let environment = add_npm_test_plugin_package(&mut builder, "0.3.0")
.with_info_file(|_| {})
.with_local_config("/dprint.json", |config| {
config.add_plugin("npm:@dprint/test-plugin@0.3.0");
})
.initialize()
.build();
let before = environment.read_file("/dprint.json").unwrap();
run_test_cli(vec!["config", "update"], &environment).unwrap();
assert_eq!(environment.read_file("/dprint.json").unwrap(), before);
let stderr = environment.take_stderr_messages();
assert!(!stderr.iter().any(|m| m.contains("Updating")), "got: {stderr:?}");
}
#[test]
fn config_update_of_an_npm_plugin_is_not_blocked_by_the_plugins_own_version() {
use crate::test_helpers::WASM_PLUGIN_BYTES;
use crate::test_helpers::create_test_npm_tarball;
let tarball_url = |version: &str| format!("https://registry.npmjs.org/@dprint/test-plugin/-/test-plugin-{version}.tgz");
let packument = json!({
"dist-tags": { "latest": "0.1.5" },
"versions": {
"0.1.0": { "dist": { "tarball": tarball_url("0.1.0") } },
"0.1.5": { "dist": { "tarball": tarball_url("0.1.5") } },
}
});
let tarball = create_test_npm_tarball(&[("package/plugin.wasm", WASM_PLUGIN_BYTES)]);
let environment = TestEnvironmentBuilder::new()
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.add_remote_file_bytes(&tarball_url("0.1.0"), tarball.clone())
.add_remote_file_bytes(&tarball_url("0.1.5"), tarball)
.with_info_file(|_| {})
.with_local_config("/dprint.json", |config| {
config.add_plugin("npm:@dprint/test-plugin@0.1.0");
})
.initialize()
.build();
run_test_cli(vec!["config", "update"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("\"npm:@dprint/test-plugin@0.1.5\""), "got: {dprint_json}");
let stderr = environment.take_stderr_messages();
assert!(stderr.iter().any(|m| m == "Updating test-plugin 0.1.0 to 0.1.5..."), "got: {stderr:?}");
}
#[test]
fn config_update_keeps_the_url_when_the_packages_latest_isnt_a_version() {
use crate::test_helpers::WASM_PLUGIN_BYTES;
use crate::test_helpers::create_test_npm_tarball;
let tarball_url = "https://registry.npmjs.org/@dprint/test-plugin/-/test-plugin-evil.tgz";
let packument = json!({
"dist-tags": { "latest": "0.3.0/plugin.json" },
"versions": { "0.3.0/plugin.json": { "dist": { "tarball": tarball_url } } }
});
let environment = TestEnvironmentBuilder::new()
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.add_remote_file_bytes(tarball_url, create_test_npm_tarball(&[("package/plugin.wasm", WASM_PLUGIN_BYTES)]))
.add_remote_wasm_plugin()
.add_remote_wasm_0_1_0_plugin()
.with_info_file(|_| {})
.add_remote_file("https://plugins.dprint.dev/dprint/test-plugin/latest.json", &test_plugin_latest_json(true))
.with_local_config("/dprint.json", |config| {
config.add_plugin("https://plugins.dprint.dev/test-plugin-0.1.0.wasm");
})
.initialize()
.build();
run_test_cli(vec!["config", "update"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(!dprint_json.contains("npm:"), "got: {dprint_json}");
assert!(dprint_json.contains("\"https://plugins.dprint.dev/test-plugin.wasm\""), "got: {dprint_json}");
let stderr = environment.take_stderr_messages();
assert!(
stderr
.iter()
.any(|m| m.contains("Failed resolving the npm package for test-plugin. Keeping its url.")
&& m.contains("contains invalid characters (got '0.3.0/plugin.json')")),
"got: {stderr:?}"
);
}
#[test]
fn config_update_only_moves_plugins_from_the_latest_files_origin() {
let mut builder = TestEnvironmentBuilder::new();
let environment = add_npm_test_plugin_package(&mut builder, "0.3.0")
.add_remote_wasm_plugin()
.add_remote_file_bytes(
"https://example.com/test-plugin-0.1.0.wasm",
crate::test_helpers::WASM_PLUGIN_0_1_0_BYTES.to_vec(),
)
.with_info_file(|_| {})
.add_remote_file("https://plugins.dprint.dev/dprint/test-plugin/latest.json", &test_plugin_latest_json(true))
.with_local_config("/dprint.json", |config| {
config.add_plugin("https://example.com/test-plugin-0.1.0.wasm");
})
.initialize()
.build();
run_test_cli(vec!["config", "update"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(!dprint_json.contains("npm:"), "got: {dprint_json}");
assert!(dprint_json.contains("\"https://plugins.dprint.dev/test-plugin.wasm\""), "got: {dprint_json}");
assert_eq!(
environment.take_stderr_messages(),
vec![
"Updating test-plugin 0.1.0 to 0.2.0...",
"Compiling https://plugins.dprint.dev/test-plugin.wasm",
]
);
}
#[test]
fn config_update_runs_plugin_config_updates_for_an_npm_plugin() {
use crate::test_helpers::WASM_PLUGIN_BYTES;
use crate::test_helpers::create_test_npm_tarball;
let tarball_url = |version: &str| format!("https://registry.npmjs.org/@dprint/test-plugin/-/test-plugin-{version}.tgz");
let packument = json!({
"dist-tags": { "latest": "0.3.0" },
"versions": {
"0.1.0": { "dist": { "tarball": tarball_url("0.1.0") } },
"0.3.0": { "dist": { "tarball": tarball_url("0.3.0") } },
}
});
let tarball = create_test_npm_tarball(&[("package/plugin.wasm", WASM_PLUGIN_BYTES)]);
let environment = TestEnvironmentBuilder::new()
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-plugin", packument.to_string().into_bytes())
.add_remote_file_bytes(&tarball_url("0.1.0"), tarball.clone())
.add_remote_file_bytes(&tarball_url("0.3.0"), tarball)
.with_info_file(|_| {})
.with_local_config("/dprint.json", |config| {
config.add_plugin("npm:@dprint/test-plugin@0.1.0").add_config_section(
"test-plugin",
r#"{
"should_set": "asdf"
}"#,
);
})
.initialize()
.build();
run_test_cli(vec!["config", "update", "--yes"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("\"npm:@dprint/test-plugin@0.3.0\""), "got: {dprint_json}");
assert!(dprint_json.contains("\"should_set\": \"new_value_wasm\""), "got: {dprint_json}");
environment.take_stderr_messages();
}
#[test]
fn config_update_resolves_the_registry_from_an_npmrc_next_to_the_config() {
use crate::test_helpers::WASM_PLUGIN_BYTES;
use crate::test_helpers::create_test_npm_tarball;
let tarball_url = |version: &str| format!("https://dprint.example.com/@dprint/test-plugin/-/test-plugin-{version}.tgz");
let packument = json!({
"dist-tags": { "latest": "0.3.0" },
"versions": {
"0.1.0": { "dist": { "tarball": tarball_url("0.1.0") } },
"0.3.0": { "dist": { "tarball": tarball_url("0.3.0") } },
}
});
let tarball = create_test_npm_tarball(&[("package/plugin.wasm", WASM_PLUGIN_BYTES)]);
let environment = TestEnvironmentBuilder::new()
.add_remote_file_bytes("https://dprint.example.com/@dprint/test-plugin", packument.to_string().into_bytes())
.add_remote_file_bytes(&tarball_url("0.1.0"), tarball.clone())
.add_remote_file_bytes(&tarball_url("0.3.0"), tarball)
.write_file("/.npmrc", "@dprint:registry=https://dprint.example.com")
.with_info_file(|_| {})
.with_local_config("/dprint.json", |config| {
config.add_plugin("npm:@dprint/test-plugin@0.1.0");
})
.initialize()
.build();
run_test_cli(vec!["config", "update"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("\"npm:@dprint/test-plugin@0.3.0\""), "got: {dprint_json}");
assert_eq!(
environment.take_stderr_messages(),
vec![
"Updating test-plugin 0.1.0 to 0.3.0...",
"Compiling /cache/npm/dprint.example.com/@dprint__test-plugin@0.3.0/plugin.wasm",
]
);
}
#[test]
fn config_update_moves_a_url_plugin_to_a_registry_from_an_npmrc() {
use crate::test_helpers::WASM_PLUGIN_BYTES;
use crate::test_helpers::create_test_npm_tarball;
let tarball_url = "https://dprint.example.com/@dprint/test-plugin/-/test-plugin-0.3.0.tgz";
let packument = json!({
"dist-tags": { "latest": "0.3.0" },
"versions": { "0.3.0": { "dist": { "tarball": tarball_url } } }
});
let environment = TestEnvironmentBuilder::new()
.add_remote_file_bytes("https://dprint.example.com/@dprint/test-plugin", packument.to_string().into_bytes())
.add_remote_file_bytes(tarball_url, create_test_npm_tarball(&[("package/plugin.wasm", WASM_PLUGIN_BYTES)]))
.add_remote_wasm_0_1_0_plugin()
.write_file("/.npmrc", "@dprint:registry=https://dprint.example.com")
.with_info_file(|_| {})
.add_remote_file("https://plugins.dprint.dev/dprint/test-plugin/latest.json", &test_plugin_latest_json(true))
.with_local_config("/dprint.json", |config| {
config.add_plugin("https://plugins.dprint.dev/test-plugin-0.1.0.wasm");
})
.initialize()
.build();
run_test_cli(vec!["config", "update"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("\"npm:@dprint/test-plugin@0.3.0\""), "got: {dprint_json}");
assert_eq!(
environment.take_stderr_messages(),
vec![
"Updating test-plugin 0.1.0 to npm:@dprint/test-plugin@0.3.0...",
"Compiling /cache/npm/dprint.example.com/@dprint__test-plugin@0.3.0/plugin.wasm",
]
);
}
#[test]
fn config_update_only_moves_plugins_from_the_info_files_origin() {
let environment = npm_info_file_builder(&["https://example.com/test-plugin-0.1.0.wasm"])
.add_remote_file_bytes(
"https://example.com/test-plugin-0.1.0.wasm",
crate::test_helpers::WASM_PLUGIN_0_1_0_BYTES.to_vec(),
)
.initialize()
.build();
run_test_cli(vec!["config", "update"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(!dprint_json.contains("npm:"), "got: {dprint_json}");
assert!(dprint_json.contains("\"https://plugins.dprint.dev/test-plugin.wasm\""), "got: {dprint_json}");
assert_eq!(
environment.take_stderr_messages(),
vec![
"Updating test-plugin 0.1.0 to 0.2.0...",
"Compiling https://plugins.dprint.dev/test-plugin.wasm",
]
);
}
#[test]
fn config_update_moves_process_plugin_to_npm_after_confirming() {
use crate::test_helpers::PROCESS_PLUGIN_ZIP_BYTES;
use crate::test_helpers::create_test_npm_tarball;
use crate::utils::get_sha256_checksum;
let zip_bytes: &[u8] = &PROCESS_PLUGIN_ZIP_BYTES;
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}" }}
}}"#,
get_sha256_checksum(zip_bytes)
);
let tarball = create_test_npm_tarball(&[("package/plugin.json", plugin_json.as_bytes()), ("package/bin.zip", zip_bytes)]);
let tarball_checksum = get_sha256_checksum(&tarball);
let packument = json!({
"dist-tags": { "latest": "0.3.0" },
"versions": { "0.3.0": { "dist": { "tarball": "https://registry.npmjs.org/@dprint/test-process/-/test-process-0.3.0.tgz" } } }
});
let environment = TestEnvironmentBuilder::new()
.add_remote_process_plugin()
.with_info_file(|info| {
info.add_plugin(TestInfoFilePlugin {
name: "test-process-plugin".to_string(),
version: "0.1.0".to_string(),
url: "https://plugins.dprint.dev/test-process.json".to_string(),
config_key: Some("test-process-plugin".to_string()),
npm: Some(crate::environment::TestInfoFileNpm {
name: "@dprint/test-process".to_string(),
..Default::default()
}),
..Default::default()
});
})
.with_default_config(|config| {
config.add_remote_process_plugin();
})
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-process", packument.to_string().into_bytes())
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-process/-/test-process-0.3.0.tgz", tarball)
.initialize()
.build();
environment.set_confirm_results(vec![Ok(Some(true))]);
run_test_cli(vec!["config", "update"], &environment).unwrap();
let expected_entry = format!("npm:@dprint/test-process@0.3.0/plugin.json@{}", tarball_checksum);
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains(&format!("\"{}\"", expected_entry)), "got: {dprint_json}");
let stderr = environment.take_stderr_messages();
assert!(
stderr.contains(&format!("The process plugin test-process-plugin 0.1.0 is moving to npm: {}", expected_entry)),
"got: {stderr:?}"
);
}
#[test]
fn config_update_keeps_a_process_plugin_on_its_url_when_the_move_is_declined() {
let packument = json!({
"dist-tags": { "latest": "0.3.0" },
"versions": { "0.3.0": { "dist": { "tarball": "https://registry.npmjs.org/@dprint/test-process/-/test-process-0.3.0.tgz" } } }
});
let environment = TestEnvironmentBuilder::new()
.add_remote_process_plugin()
.with_info_file(|info| {
info.add_plugin(TestInfoFilePlugin {
name: "test-process-plugin".to_string(),
version: "0.1.0".to_string(),
url: "https://plugins.dprint.dev/test-process.json".to_string(),
config_key: Some("test-process-plugin".to_string()),
npm: Some(crate::environment::TestInfoFileNpm {
name: "@dprint/test-process".to_string(),
..Default::default()
}),
..Default::default()
});
})
.with_default_config(|config| {
config.add_remote_process_plugin();
})
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/test-process", packument.to_string().into_bytes())
.add_remote_file_bytes(
"https://registry.npmjs.org/@dprint/test-process/-/test-process-0.3.0.tgz",
crate::test_helpers::create_test_npm_tarball(&[("package/plugin.json", b"{}")]),
)
.initialize()
.build();
environment.set_confirm_results(vec![Ok(Some(false))]);
run_test_cli(vec!["config", "update"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("https://plugins.dprint.dev/test-process.json"), "got: {dprint_json}");
assert!(!dprint_json.contains("npm:"), "got: {dprint_json}");
let _ = environment.take_stderr_messages();
}
#[test]
fn config_update_moves_url_plugin_to_a_subpath_within_a_package() {
use crate::test_helpers::WASM_PLUGIN_BYTES;
use crate::test_helpers::create_test_npm_tarball;
let packument = json!({
"dist-tags": { "latest": "1.0.0" },
"versions": { "1.0.0": { "dist": { "tarball": "https://registry.npmjs.org/@dprint/example/-/example-1.0.0.tgz" } } }
});
let environment = TestEnvironmentBuilder::new()
.add_remote_wasm_plugin()
.add_remote_wasm_0_1_0_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",
"npm": { "name": "@dprint/example", "path": "test-plugin/plugin.wasm" },
})
.to_string(),
)
.with_info_file(|_| {})
.with_local_config("/dprint.json", |config| {
config.add_plugin("https://plugins.dprint.dev/test-plugin-0.1.0.wasm");
})
.add_remote_file_bytes("https://registry.npmjs.org/@dprint/example", packument.to_string().into_bytes())
.add_remote_file_bytes(
"https://registry.npmjs.org/@dprint/example/-/example-1.0.0.tgz",
create_test_npm_tarball(&[("package/test-plugin/plugin.wasm", WASM_PLUGIN_BYTES)]),
)
.initialize()
.build();
run_test_cli(vec!["config", "update"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(
dprint_json.contains("\"npm:@dprint/example@1.0.0/test-plugin/plugin.wasm\""),
"got: {dprint_json}"
);
assert_eq!(
environment.take_stderr_messages(),
vec![
"Updating test-plugin 0.1.0 to npm:@dprint/example@1.0.0/test-plugin/plugin.wasm...",
"Compiling /cache/npm/registry.npmjs.org/@dprint__example@1.0.0/test-plugin/plugin.wasm",
]
);
}
#[test]
fn config_update_leaves_a_local_plugin_alone() {
let environment = npm_info_file_builder(&[])
.write_file("/plugins/test-plugin.wasm", crate::test_helpers::WASM_PLUGIN_BYTES)
.with_local_config("/dprint.json", |config| {
config.add_plugin("./plugins/test-plugin.wasm");
})
.initialize()
.build();
run_test_cli(vec!["config", "update"], &environment).unwrap();
let dprint_json = environment.read_file("/dprint.json").unwrap();
assert!(dprint_json.contains("\"./plugins/test-plugin.wasm\""), "got: {dprint_json}");
assert_eq!(environment.take_stderr_messages(), Vec::<String>::new());
}
#[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:?}"
);
}
}