use std::collections::HashSet;
use std::collections::VecDeque;
use std::path::PathBuf;
use anyhow::Result;
use anyhow::bail;
use dprint_core::async_runtime::future;
use dprint_core::plugins::wasm::{self};
use jsonc_parser::cst::CstInputValue;
use jsonc_parser::cst::CstRootNode;
use crate::environment::DirEntry;
use crate::environment::Environment;
use crate::plugins::InfoFile;
use crate::plugins::InfoFilePluginInfo;
use crate::plugins::MinimumDependencyAgeError;
use crate::plugins::ResolveNpmLatestOptions;
use crate::plugins::read_info_file;
use crate::plugins::resolve_dependency_age_cutoff;
use crate::utils::DependencyAgeCutoff;
use crate::utils::DirEntriesHint;
use crate::utils::GitIgnoreTree;
use crate::utils::GitIgnoreTreeOptions;
use crate::utils::MinimumDependencyAgeArg;
use crate::utils::MultiSelectItem;
use crate::utils::resolve_global_gitignore_lines;
const MAX_SCAN_FILES: usize = 1000;
const MAX_SCAN_DIRS: usize = 1000;
#[derive(Default)]
pub struct GetInitConfigFileTextOptions {
pub non_interactive: bool,
pub minimum_dependency_age: Option<MinimumDependencyAgeArg>,
pub config_dir: Option<PathBuf>,
}
pub struct GetInitPluginsToAddOptions {
pub existing_plugin_names: HashSet<String>,
pub minimum_dependency_age: Option<MinimumDependencyAgeArg>,
pub config_dir: Option<PathBuf>,
}
pub enum InitPluginsToAdd {
AllPluginsConfigured,
Entries(Vec<String>),
}
pub async fn get_init_config_file_text(environment: &impl Environment, options: GetInitConfigFileTextOptions) -> Result<String> {
let info = read_plugin_info_file(environment).await;
let selected_plugins = if let Some(info) = info {
let latest_plugins = info.latest_plugins;
let mut project_files = scan_project_files(environment);
project_files.extensions.insert("json".to_string());
let defaults = compute_default_selections(&latest_plugins, &project_files, &[]);
let mut selected_indexes = if options.non_interactive {
defaults.iter().enumerate().filter_map(|(i, on)| on.then_some(i)).collect::<Vec<_>>()
} else {
let order = display_order(&defaults, &[]);
let prompt_message = "Select plugins (space to toggle, type to filter, enter to finish):";
let items = order
.iter()
.map(|&i| MultiSelectItem::new(plugin_display_text(&latest_plugins[i]), defaults[i]))
.collect::<Vec<_>>();
let chosen = environment.get_multi_selection(prompt_message, 0, items)?;
chosen.into_iter().map(|display_index| order[display_index]).collect::<Vec<_>>()
};
selected_indexes.sort_unstable();
let age_cutoff = resolve_dependency_age_cutoff(options.minimum_dependency_age.as_ref(), options.config_dir.as_deref(), environment);
let entries = future::join_all(
selected_indexes
.iter()
.map(|&index| resolve_plugin_entry(&latest_plugins[index], age_cutoff.as_ref(), environment)),
)
.await
.into_iter()
.collect::<Result<Vec<_>>>()?;
let selected_plugins = selected_indexes
.into_iter()
.zip(entries)
.map(|(index, entry)| {
let plugin = latest_plugins[index].clone();
let config = build_plugin_config(&plugin, &project_files);
SelectedPlugin { plugin, config, entry }
})
.collect::<Vec<_>>();
Some(selected_plugins)
} else {
None
};
let json_text = match selected_plugins {
Some(selected_plugins) if !selected_plugins.is_empty() => render_config_file(&selected_plugins),
Some(_) => "{\n \"excludes\": [],\n \"plugins\": [\n // specify plugin urls here\n ]\n}\n".to_string(),
None => "{\n \"excludes\": [\n \"**/*-lock.json\"\n ],\n \"plugins\": [\n // specify plugin urls here\n ]\n}\n".to_string(),
};
Ok(json_text)
}
pub async fn get_init_plugins_to_add(environment: &impl Environment, options: GetInitPluginsToAddOptions) -> Result<InitPluginsToAdd> {
let Some(info) = read_plugin_info_file(environment).await else {
bail!("Could not get the latest plugin info, so there's nothing to select from.");
};
let latest_plugins = info.latest_plugins;
let already_configured = latest_plugins
.iter()
.map(|plugin| options.existing_plugin_names.contains(&plugin.name))
.collect::<Vec<_>>();
if already_configured.iter().all(|configured| *configured) {
return Ok(InitPluginsToAdd::AllPluginsConfigured);
}
let mut project_files = scan_project_files(environment);
project_files.extensions.insert("json".to_string());
let defaults = compute_default_selections(&latest_plugins, &project_files, &already_configured);
let order = display_order(&defaults, &already_configured);
let prompt_message = "Select plugins to add (space to toggle, type to filter, enter to finish):";
let items = order
.iter()
.map(|&i| {
if already_configured[i] {
MultiSelectItem::non_selectable(format!("{} — already in config", plugin_display_text(&latest_plugins[i])))
} else {
MultiSelectItem::new(plugin_display_text(&latest_plugins[i]), defaults[i])
}
})
.collect::<Vec<_>>();
let chosen = environment.get_multi_selection(prompt_message, 0, items)?;
let mut selected_indexes = chosen.into_iter().map(|display_index| order[display_index]).collect::<Vec<_>>();
selected_indexes.sort_unstable();
let age_cutoff = resolve_dependency_age_cutoff(options.minimum_dependency_age.as_ref(), options.config_dir.as_deref(), environment);
let entries = future::join_all(
selected_indexes
.iter()
.map(|&index| resolve_plugin_entry(&latest_plugins[index], age_cutoff.as_ref(), environment)),
)
.await
.into_iter()
.collect::<Result<Vec<_>>>()?;
Ok(InitPluginsToAdd::Entries(entries))
}
async fn read_plugin_info_file(environment: &impl Environment) -> Option<InfoFile> {
match read_info_file(environment).await {
Ok(info) => {
if wasm::PLUGIN_SYSTEM_SCHEMA_VERSION < info.plugin_system_schema_version {
log_error!(
environment,
concat!(
"You are using an old version of dprint so the latest plugins may not be suggested. ",
"Consider upgrading to support new plugins. ",
"Plugin system schema version is {}, latest is {}."
),
wasm::PLUGIN_SYSTEM_SCHEMA_VERSION,
info.plugin_system_schema_version,
);
None
} else {
Some(info)
}
}
Err(err) => {
log_error!(
environment,
concat!(
"There was a problem getting the latest plugin info, so the latest plugins may not be suggested. ",
"Error: {}"
),
err,
);
None
}
}
}
struct SelectedPlugin {
plugin: InfoFilePluginInfo,
config: serde_json::Value,
entry: String,
}
async fn resolve_plugin_entry(plugin: &InfoFilePluginInfo, age_cutoff: Option<&DependencyAgeCutoff>, environment: &impl Environment) -> Result<String> {
let options = ResolveNpmLatestOptions {
force_checksum: false,
base_dir: Some(environment.cwd()),
minimum_dependency_age: age_cutoff.cloned(),
};
match plugin.resolve_npm(environment, options).await {
Some(Ok(resolved)) => Ok(resolved.config_file_entry()),
Some(Err(err)) if err.downcast_ref::<MinimumDependencyAgeError>().is_some() => Err(err),
Some(Err(err)) => {
log_warn!(
environment,
"Failed resolving the npm package for {}. Using its url instead. {:#}",
plugin.name,
err
);
Ok(plugin.full_url_no_wasm_checksum())
}
None => Ok(plugin.full_url_no_wasm_checksum()),
}
}
fn render_config_file(selected_plugins: &[SelectedPlugin]) -> String {
let root = CstRootNode::parse("{\n}", &Default::default()).unwrap();
let root_obj = root.object_value_or_set();
for SelectedPlugin { plugin, config, .. } in selected_plugins {
if let Some(config_key) = &plugin.config_key
&& !config_key.is_empty()
{
let is_empty = config.as_object().is_none_or(|obj| obj.is_empty());
let prop = root_obj.append(config_key, to_cst_input(config.clone()));
if is_empty && let Some(object) = prop.value().and_then(|value| value.as_object()) {
object.ensure_multiline();
}
}
}
let excludes = get_unique_items(
selected_plugins
.iter()
.flat_map(|selected| selected.plugin.config_excludes.iter().cloned())
.collect::<Vec<_>>(),
);
let excludes_prop = root_obj.append("excludes", CstInputValue::Array(excludes.iter().cloned().map(CstInputValue::String).collect()));
if !excludes.is_empty()
&& let Some(array) = excludes_prop.value().and_then(|value| value.as_array())
{
array.ensure_multiline();
}
let entries = selected_plugins.iter().map(|selected| selected.entry.clone()).collect::<Vec<_>>();
let plugins_prop = root_obj.append("plugins", CstInputValue::Array(entries.into_iter().map(CstInputValue::String).collect()));
if let Some(array) = plugins_prop.value().and_then(|value| value.as_array()) {
array.ensure_multiline();
}
format!("{root}\n")
}
fn to_cst_input(value: serde_json::Value) -> CstInputValue {
use serde_json::Value;
match value {
Value::Null => CstInputValue::Null,
Value::Bool(value) => CstInputValue::Bool(value),
Value::Number(value) => CstInputValue::Number(value.to_string()),
Value::String(value) => CstInputValue::String(value),
Value::Array(values) => CstInputValue::Array(values.into_iter().map(to_cst_input).collect()),
Value::Object(entries) => CstInputValue::Object(entries.into_iter().map(|(key, value)| (key, to_cst_input(value))).collect()),
}
}
struct ProjectFiles {
extensions: HashSet<String>,
file_names: HashSet<String>,
}
fn compute_default_selections(plugins: &[InfoFilePluginInfo], project_files: &ProjectFiles, already_configured: &[bool]) -> Vec<bool> {
let is_already_configured = |index: usize| already_configured.get(index).copied().unwrap_or(false);
let mut claimed_extensions: HashSet<String> = HashSet::new();
let mut claimed_file_names: HashSet<String> = HashSet::new();
let mut used_config_keys: HashSet<&str> = HashSet::new();
let mut selected = vec![false; plugins.len()];
for (i, plugin) in plugins.iter().enumerate() {
if !is_already_configured(i) {
continue;
}
claimed_extensions.extend(present_extensions(plugin, project_files));
claimed_file_names.extend(present_file_names(plugin, project_files));
if let Some(config_key) = config_key(plugin) {
used_config_keys.insert(config_key);
}
}
for (i, plugin) in plugins.iter().enumerate() {
if is_already_configured(i) || plugin.never_preselect {
continue;
}
let present_extensions = present_extensions(plugin, project_files);
let present_file_names = present_file_names(plugin, project_files);
let matches_present_file = !present_extensions.is_empty() || !present_file_names.is_empty();
if plugin.additive && !matches_present_file {
continue;
}
if !plugin.additive {
let claims_unclaimed =
present_extensions.iter().any(|ext| !claimed_extensions.contains(ext)) || present_file_names.iter().any(|name| !claimed_file_names.contains(name));
if !claims_unclaimed {
continue;
}
}
if let Some(config_key) = config_key(plugin)
&& !used_config_keys.insert(config_key)
{
continue;
}
selected[i] = true;
if !plugin.additive {
claimed_extensions.extend(present_extensions);
claimed_file_names.extend(present_file_names);
}
}
selected
}
fn present_extensions(plugin: &InfoFilePluginInfo, project_files: &ProjectFiles) -> Vec<String> {
match_extensions(plugin)
.into_iter()
.filter(|ext| project_files.extensions.contains(ext))
.collect()
}
fn present_file_names(plugin: &InfoFilePluginInfo, project_files: &ProjectFiles) -> Vec<String> {
match_file_names(plugin)
.into_iter()
.filter(|name| project_files.file_names.contains(*name))
.map(|name| name.to_string())
.collect()
}
fn display_order(defaults: &[bool], already_configured: &[bool]) -> Vec<usize> {
let is_already_configured = |index: usize| already_configured.get(index).copied().unwrap_or(false);
let selected = (0..defaults.len()).filter(|&i| defaults[i] && !is_already_configured(i));
let unselected = (0..defaults.len()).filter(|&i| !defaults[i] && !is_already_configured(i));
let already_configured = (0..defaults.len()).filter(|&i| is_already_configured(i));
selected.chain(unselected).chain(already_configured).collect()
}
fn match_extensions(plugin: &InfoFilePluginInfo) -> Vec<String> {
let mut extensions = plugin.file_extensions.iter().map(|ext| ext.to_lowercase()).collect::<Vec<_>>();
for item in &plugin.config_items {
extensions.extend(item.file_extensions.iter().map(|ext| ext.to_lowercase()));
}
extensions
}
fn match_file_names(plugin: &InfoFilePluginInfo) -> Vec<&str> {
let mut file_names = plugin.file_names.iter().map(String::as_str).collect::<Vec<_>>();
for item in &plugin.config_items {
file_names.extend(item.file_names.iter().map(String::as_str));
}
file_names
}
fn config_key(plugin: &InfoFilePluginInfo) -> Option<&str> {
plugin.config_key.as_deref().filter(|key| !key.is_empty())
}
fn matches_project_files(file_extensions: &[String], file_names: &[String], project_files: &ProjectFiles) -> bool {
file_extensions.iter().any(|ext| project_files.extensions.contains(&ext.to_lowercase()))
|| file_names.iter().any(|name| project_files.file_names.contains(name))
}
fn plugin_display_text(plugin: &InfoFilePluginInfo) -> String {
let mut details = display_file_matches(plugin);
if plugin.additive {
details.push("runs in addition to other plugins".to_string());
}
if details.is_empty() {
plugin.name.clone()
} else {
format!("{} ({})", plugin.name, details.join(", "))
}
}
fn display_file_matches(plugin: &InfoFilePluginInfo) -> Vec<String> {
let extensions = display_extensions(plugin);
if !extensions.is_empty() {
return extensions.iter().map(|ext| format!(".{}", ext)).collect();
}
get_unique_items(match_file_names(plugin).into_iter().map(ToOwned::to_owned).collect())
}
fn display_extensions(plugin: &InfoFilePluginInfo) -> Vec<String> {
if !plugin.file_extensions.is_empty() {
return plugin.file_extensions.clone();
}
let mut extensions = Vec::new();
for item in &plugin.config_items {
for ext in &item.file_extensions {
if !extensions.contains(ext) {
extensions.push(ext.clone());
}
}
}
extensions
}
fn build_plugin_config(plugin: &InfoFilePluginInfo, project_files: &ProjectFiles) -> serde_json::Value {
let mut config = match plugin.default_config.clone() {
Some(value @ serde_json::Value::Object(_)) => value,
_ => serde_json::Value::Object(Default::default()),
};
for item in &plugin.config_items {
if matches_project_files(&item.file_extensions, &item.file_names, project_files) {
deep_merge(&mut config, item.config.clone());
}
}
config
}
fn deep_merge(base: &mut serde_json::Value, other: serde_json::Value) {
use serde_json::Value;
match other {
Value::Object(other_map) if base.is_object() => {
let base_map = base.as_object_mut().unwrap();
for (key, value) in other_map {
match base_map.get_mut(&key) {
Some(existing) => deep_merge(existing, value),
None => {
base_map.insert(key, value);
}
}
}
}
Value::Array(mut other_items) if base.is_array() => {
base.as_array_mut().unwrap().append(&mut other_items);
}
other => *base = other,
}
}
fn scan_project_files(environment: &impl Environment) -> ProjectFiles {
let mut extensions = HashSet::new();
let mut file_names = HashSet::new();
let mut files_remaining = MAX_SCAN_FILES;
let mut dirs_remaining = MAX_SCAN_DIRS;
let mut pending_dirs = VecDeque::from([environment.cwd().into_path_buf()]);
let mut gitignores = GitIgnoreTree::new(
environment.clone(),
GitIgnoreTreeOptions {
include_paths: Vec::new(),
global_gitignore_lines: resolve_global_gitignore_lines(environment),
},
);
'outer: while let Some(dir) = pending_dirs.pop_front() {
if dirs_remaining == 0 {
break;
}
dirs_remaining -= 1;
let Ok(entries) = environment.dir_info(&dir) else {
continue; };
let gitignore = gitignores.get_resolved_git_ignore_for_dir_children(&dir, DirEntriesHint::from_dir_entries(&entries));
let is_ignored = |path: &PathBuf, is_dir: bool| gitignore.as_ref().is_some_and(|gitignore| gitignore.is_ignored(path, is_dir));
for entry in entries {
match entry {
DirEntry::Directory(path) => {
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
continue;
};
if !is_ignored_dir(name) && !is_ignored(&path, true) {
pending_dirs.push_back(path);
}
}
DirEntry::File { name, path } => {
if is_ignored(&path, false) {
continue;
}
if files_remaining == 0 {
break 'outer;
}
files_remaining -= 1;
file_names.insert(name.to_string_lossy().into_owned());
if let Some(ext) = path.extension().and_then(|ext| ext.to_str()) {
extensions.insert(ext.to_lowercase());
}
}
}
}
}
ProjectFiles { extensions, file_names }
}
fn is_ignored_dir(name: &str) -> bool {
matches!(name, ".git" | "node_modules" | "target" | "vendor" | "dist" | "build" | "out" | "bin" | "obj")
}
fn get_unique_items<T>(vec: Vec<T>) -> Vec<T>
where
T: PartialEq,
{
let mut new_vec = Vec::new();
for item in vec {
if !new_vec.contains(&item) {
new_vec.push(item);
}
}
new_vec
}
#[cfg(test)]
mod test {
use super::*;
use crate::environment::TestEnvironment;
use crate::environment::TestEnvironmentBuilder;
use crate::environment::TestInfoFileConfigItem;
use crate::environment::TestInfoFileMatch;
use crate::environment::TestInfoFileNpm;
use crate::environment::TestInfoFilePlugin;
use crate::plugins::InfoFileConfigItem;
use pretty_assertions::assert_eq;
fn exec_info_plugin() -> TestInfoFilePlugin {
TestInfoFilePlugin {
name: "dprint-plugin-exec".to_string(),
version: "0.5.0".to_string(),
url: "https://plugins.dprint.dev/exec-0.5.0.json".to_string(),
config_key: Some("exec".to_string()),
file_extensions: vec![],
config_excludes: vec![],
checksum: Some("checksum".to_string()),
default_config: Some(serde_json::json!({ "cwd": "${configDir}" })),
config_items: vec![
TestInfoFileConfigItem {
file_match: TestInfoFileMatch {
file_extensions: vec!["rs".to_string()],
file_names: vec![],
},
config: serde_json::json!({ "commands": [{ "command": "rustfmt", "exts": ["rs"] }] }),
},
TestInfoFileConfigItem {
file_match: TestInfoFileMatch {
file_extensions: vec!["go".to_string()],
file_names: vec![],
},
config: serde_json::json!({ "commands": [{ "command": "gofmt", "exts": ["go"] }] }),
},
],
..Default::default()
}
}
fn info_plugin_with_extensions(file_extensions: Vec<&str>, config_item_extensions: Vec<Vec<&str>>) -> InfoFilePluginInfo {
InfoFilePluginInfo {
name: "dprint-plugin-exec".to_string(),
version: "0.5.0".to_string(),
url: "https://plugins.dprint.dev/exec-0.5.0.json".to_string(),
config_key: Some("exec".to_string()),
file_extensions: file_extensions.into_iter().map(String::from).collect(),
file_names: vec![],
config_excludes: vec![],
checksum: None,
additive: false,
never_preselect: false,
npm: None,
default_config: None,
config_items: config_item_extensions
.into_iter()
.map(|extensions| InfoFileConfigItem {
file_extensions: extensions.into_iter().map(String::from).collect(),
file_names: vec![],
config: serde_json::Value::Object(Default::default()),
})
.collect(),
}
}
#[test]
fn plugin_display_text_falls_back_to_config_item_extensions() {
let plugin = info_plugin_with_extensions(vec!["ts", "tsx"], vec![vec!["rs"]]);
assert_eq!(plugin_display_text(&plugin), "dprint-plugin-exec (.ts, .tsx)");
let plugin = info_plugin_with_extensions(vec![], vec![vec!["rs"], vec!["go", "rs"]]);
assert_eq!(plugin_display_text(&plugin), "dprint-plugin-exec (.rs, .go)");
let plugin = info_plugin_with_extensions(vec![], vec![]);
assert_eq!(plugin_display_text(&plugin), "dprint-plugin-exec");
}
#[test]
fn should_scaffold_config_from_file_name_match() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info.add_plugin(TestInfoFilePlugin {
name: "dprint-plugin-exec".to_string(),
version: "0.5.0".to_string(),
url: "https://plugins.dprint.dev/exec-0.5.0.json".to_string(),
config_key: Some("exec".to_string()),
file_extensions: vec![],
config_excludes: vec![],
checksum: Some("checksum".to_string()),
config_items: vec![TestInfoFileConfigItem {
file_match: TestInfoFileMatch {
file_extensions: vec![],
file_names: vec!["Makefile".to_string()],
},
config: serde_json::json!({ "commands": [{ "command": "make", "exts": [] }] }),
}],
..Default::default()
});
})
.write_file("/Makefile", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"exec": {
"commands": [
{
"command": "make",
"exts": []
}
]
},
"excludes": [],
"plugins": [
"https://plugins.dprint.dev/exec-0.5.0.json@checksum"
]
}
"#
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_let_config_item_override_default_config() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info.add_plugin(TestInfoFilePlugin {
name: "dprint-plugin-exec".to_string(),
version: "0.5.0".to_string(),
url: "https://plugins.dprint.dev/exec-0.5.0.json".to_string(),
config_key: Some("exec".to_string()),
file_extensions: vec![],
config_excludes: vec![],
checksum: Some("checksum".to_string()),
default_config: Some(serde_json::json!({ "cwd": "default", "indentWidth": 2 })),
config_items: vec![TestInfoFileConfigItem {
file_match: TestInfoFileMatch {
file_extensions: vec!["rs".to_string()],
file_names: vec![],
},
config: serde_json::json!({ "cwd": "${configDir}", "commands": [{ "command": "rustfmt", "exts": ["rs"] }] }),
}],
..Default::default()
});
})
.write_file("/main.rs", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"exec": {
"cwd": "${configDir}",
"indentWidth": 2,
"commands": [
{
"command": "rustfmt",
"exts": ["rs"]
}
]
},
"excludes": [],
"plugins": [
"https://plugins.dprint.dev/exec-0.5.0.json@checksum"
]
}
"#
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
fn json_info_plugin() -> TestInfoFilePlugin {
TestInfoFilePlugin {
name: "dprint-plugin-json".to_string(),
version: "0.19.2".to_string(),
url: "https://plugins.dprint.dev/json-0.19.2.wasm".to_string(),
config_key: Some("json".to_string()),
file_extensions: vec!["json".to_string()],
config_excludes: vec![],
..Default::default()
}
}
fn additive_info_plugin() -> TestInfoFilePlugin {
TestInfoFilePlugin {
name: "dprint-plugin-additive".to_string(),
version: "0.1.0".to_string(),
url: "https://plugins.dprint.dev/additive-0.1.0.wasm".to_string(),
config_key: Some("additivePlugin".to_string()),
file_extensions: vec![],
file_names: Some(vec!["package.json".to_string()]),
config_excludes: vec![],
additive: true,
..Default::default()
}
}
#[test]
fn should_pre_select_additive_plugin_alongside_the_plugin_that_claims_the_file() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info.add_plugin(json_info_plugin()).add_plugin(additive_info_plugin());
})
.write_file("/package.json", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(
&environment,
GetInitConfigFileTextOptions {
non_interactive: true,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(
text,
r#"{
"json": {
},
"additivePlugin": {
},
"excludes": [],
"plugins": [
"https://plugins.dprint.dev/json-0.19.2.wasm",
"https://plugins.dprint.dev/additive-0.1.0.wasm"
]
}
"#
);
});
}
#[test]
fn should_not_let_an_additive_plugin_claim_files_from_a_later_plugin() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
let mut additive = additive_info_plugin();
additive.file_extensions = vec!["json".to_string()];
info.add_plugin(additive).add_plugin(json_info_plugin());
})
.write_file("/data.json", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(
&environment,
GetInitConfigFileTextOptions {
non_interactive: true,
..Default::default()
},
)
.await
.unwrap();
assert!(text.contains("json-0.19.2.wasm"), "{text}");
assert!(text.contains("additive-0.1.0.wasm"), "{text}");
});
}
#[test]
fn should_not_pre_select_an_additive_plugin_that_shares_a_config_key() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
let mut additive = additive_info_plugin();
additive.config_key = Some("json".to_string());
info.add_plugin(json_info_plugin()).add_plugin(additive);
})
.write_file("/package.json", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(
&environment,
GetInitConfigFileTextOptions {
non_interactive: true,
..Default::default()
},
)
.await
.unwrap();
assert!(text.contains("json-0.19.2.wasm"), "{text}");
assert!(!text.contains("additive-0.1.0.wasm"), "{text}");
});
}
#[test]
fn should_not_pre_select_additive_plugin_when_its_files_arent_present() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info.add_plugin(json_info_plugin()).add_plugin(additive_info_plugin());
})
.write_file("/data.json", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(
&environment,
GetInitConfigFileTextOptions {
non_interactive: true,
..Default::default()
},
)
.await
.unwrap();
assert!(!text.contains("additivePlugin"), "{text}");
assert!(text.contains("json-0.19.2.wasm"), "{text}");
});
}
#[test]
fn plugin_display_text_marks_additive_plugins() {
let mut plugin = info_plugin_with_extensions(vec![], vec![]);
plugin.name = "dprint-plugin-additive".to_string();
plugin.additive = true;
plugin.file_names = vec!["package.json".to_string()];
assert_eq!(
plugin_display_text(&plugin),
"dprint-plugin-additive (package.json, runs in addition to other plugins)"
);
plugin.file_extensions = vec!["json".to_string()];
assert_eq!(
plugin_display_text(&plugin),
"dprint-plugin-additive (.json, runs in addition to other plugins)"
);
}
#[test]
fn should_not_pre_select_a_never_preselect_plugin() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
let mut plugin = wasm_plugin("a", "a", &["txt"]);
plugin.never_preselect = true;
info.add_plugin(plugin);
})
.write_file("/notes.txt", "")
.build();
environment.clone().run_in_runtime({
let environment = environment.clone();
async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(environment.take_multi_selection_items(), vec!["[ ] a (.txt)"]);
assert!(!text.contains("a-1.0.0.wasm"), "{text}");
environment.take_stderr_messages();
}
});
}
#[test]
fn should_not_let_a_never_preselect_plugin_claim_anything_from_a_later_plugin() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
let mut plugin = wasm_plugin("a", "shared", &["txt"]);
plugin.never_preselect = true;
info.add_plugin(plugin).add_plugin(wasm_plugin("b", "shared", &["txt"]));
})
.write_file("/notes.txt", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(
&environment,
GetInitConfigFileTextOptions {
non_interactive: true,
..Default::default()
},
)
.await
.unwrap();
assert!(text.contains("b-1.0.0.wasm"), "{text}");
assert!(!text.contains("a-1.0.0.wasm"), "{text}");
});
}
#[test]
fn should_let_a_never_preselect_plugin_a_config_file_has_claim_its_file_types() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
let mut plugin = wasm_plugin("a", "a", &["txt"]);
plugin.never_preselect = true;
info.add_plugin(plugin).add_plugin(wasm_plugin("b", "b", &["txt"]));
})
.write_file("/notes.txt", "")
.build();
environment.clone().run_in_runtime({
let environment = environment.clone();
async move {
let plugins = get_init_plugins_to_add(
&environment,
GetInitPluginsToAddOptions {
existing_plugin_names: HashSet::from(["a".to_string()]),
minimum_dependency_age: None,
config_dir: None,
},
)
.await
.unwrap();
assert_eq!(
environment.take_multi_selection_items(),
vec!["[ ] b (.txt)", "[x] a (.txt) — already in config (locked)"]
);
let InitPluginsToAdd::Entries(entries) = plugins else {
unreachable!();
};
assert!(entries.is_empty());
environment.take_stderr_messages();
}
});
}
#[test]
fn should_auto_select_exec_non_interactive() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info.add_plugin(exec_info_plugin());
})
.write_file("/main.rs", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(
&environment,
GetInitConfigFileTextOptions {
non_interactive: true,
..Default::default()
},
)
.await
.unwrap();
assert!(text.contains("\"command\": \"rustfmt\""), "{text}");
assert!(text.contains("exec-0.5.0.json@checksum"), "{text}");
assert_eq!(environment.take_stderr_messages(), Vec::<String>::new());
});
}
#[test]
fn should_scaffold_exec_config_from_matched_file() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info.add_plugin(exec_info_plugin());
})
.write_file("/main.rs", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"exec": {
"cwd": "${configDir}",
"commands": [
{
"command": "rustfmt",
"exts": ["rs"]
}
]
},
"excludes": [],
"plugins": [
"https://plugins.dprint.dev/exec-0.5.0.json@checksum"
]
}
"#
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_concatenate_config_items_in_info_order() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info.add_plugin(exec_info_plugin());
})
.write_file("/main.rs", "")
.write_file("/lib.go", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"exec": {
"cwd": "${configDir}",
"commands": [
{
"command": "rustfmt",
"exts": ["rs"]
},
{
"command": "gofmt",
"exts": ["go"]
}
]
},
"excludes": [],
"plugins": [
"https://plugins.dprint.dev/exec-0.5.0.json@checksum"
]
}
"#
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_apply_only_default_config_when_no_items_match() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info.add_plugin(exec_info_plugin());
})
.build();
environment.set_multi_selection_result(vec![0]);
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"exec": {
"cwd": "${configDir}"
},
"excludes": [],
"plugins": [
"https://plugins.dprint.dev/exec-0.5.0.json@checksum"
]
}
"#
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn deep_merge_combines_objects_and_arrays() {
let mut base = serde_json::json!({ "a": 1, "list": [1, 2], "nested": { "x": 1 } });
deep_merge(&mut base, serde_json::json!({ "a": 2, "list": [3], "nested": { "y": 2 }, "b": 3 }));
assert_eq!(
base,
serde_json::json!({ "a": 2, "list": [1, 2, 3], "nested": { "x": 1, "y": 2 }, "b": 3 })
);
}
fn wasm_plugin(name: &str, config_key: &str, extensions: &[&str]) -> TestInfoFilePlugin {
TestInfoFilePlugin {
name: name.to_string(),
version: "1.0.0".to_string(),
url: format!("https://plugins.dprint.dev/{}-1.0.0.wasm", name),
config_key: Some(config_key.to_string()),
file_extensions: extensions.iter().map(|e| e.to_string()).collect(),
config_excludes: vec![],
..Default::default()
}
}
#[test]
fn should_get_plugins_to_add_to_an_existing_config_file() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info
.add_plugin(wasm_plugin("a", "a", &["ts"]))
.add_plugin(wasm_plugin("b", "b", &["json"]))
.add_plugin(wasm_plugin("c", "c", &["md"]));
})
.write_file("/main.ts", "")
.write_file("/data.json", "")
.build();
environment.clone().run_in_runtime({
let environment = environment.clone();
async move {
let plugins = get_init_plugins_to_add(
&environment,
GetInitPluginsToAddOptions {
existing_plugin_names: HashSet::from(["a".to_string()]),
minimum_dependency_age: None,
config_dir: None,
},
)
.await
.unwrap();
assert_eq!(
environment.take_multi_selection_items(),
vec!["[x] b (.json)", "[ ] c (.md)", "[x] a (.ts) — already in config (locked)"]
);
let InitPluginsToAdd::Entries(entries) = plugins else {
unreachable!();
};
assert_eq!(entries, vec!["https://plugins.dprint.dev/b-1.0.0.wasm".to_string()]);
assert_eq!(
environment.take_stderr_messages(),
vec!["Select plugins to add (space to toggle, type to filter, enter to finish):"]
);
}
});
}
#[test]
fn should_not_suggest_a_plugin_for_a_file_type_an_existing_plugin_handles() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info.add_plugin(wasm_plugin("a", "a", &["ts"])).add_plugin(wasm_plugin("b", "b", &["ts"]));
})
.write_file("/main.ts", "")
.build();
environment.clone().run_in_runtime({
let environment = environment.clone();
async move {
let plugins = get_init_plugins_to_add(
&environment,
GetInitPluginsToAddOptions {
existing_plugin_names: HashSet::from(["a".to_string()]),
minimum_dependency_age: None,
config_dir: None,
},
)
.await
.unwrap();
assert_eq!(
environment.take_multi_selection_items(),
vec!["[ ] b (.ts)", "[x] a (.ts) — already in config (locked)"]
);
let InitPluginsToAdd::Entries(entries) = plugins else {
unreachable!();
};
assert!(entries.is_empty());
environment.take_stderr_messages();
}
});
}
#[test]
fn should_not_prompt_when_the_config_file_has_every_plugin() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info.add_plugin(wasm_plugin("a", "a", &["ts"]));
})
.write_file("/main.ts", "")
.build();
environment.clone().run_in_runtime({
let environment = environment.clone();
async move {
let plugins = get_init_plugins_to_add(
&environment,
GetInitPluginsToAddOptions {
existing_plugin_names: HashSet::from(["a".to_string()]),
minimum_dependency_age: None,
config_dir: None,
},
)
.await
.unwrap();
assert!(matches!(plugins, InitPluginsToAdd::AllPluginsConfigured));
assert!(environment.take_multi_selection_items().is_empty());
}
});
}
#[test]
fn display_order_lists_selected_first() {
assert_eq!(display_order(&[false, true, false, true], &[]), vec![1, 3, 0, 2]);
assert_eq!(display_order(&[true, true], &[]), vec![0, 1]);
assert_eq!(display_order(&[false, false, false], &[]), vec![0, 1, 2]);
assert_eq!(display_order(&[], &[]), Vec::<usize>::new());
assert_eq!(display_order(&[false, true, false], &[true, false, false]), vec![1, 2, 0]);
}
#[test]
fn should_select_only_the_first_plugin_for_a_shared_extension() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info.add_plugin(wasm_plugin("a", "a", &["ts"])).add_plugin(wasm_plugin("b", "b", &["ts"]));
})
.write_file("/file.ts", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert!(text.contains("a-1.0.0.wasm"), "{text}");
assert!(!text.contains("b-1.0.0.wasm"), "{text}");
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_select_both_plugins_on_partial_extension_overlap() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info
.add_plugin(wasm_plugin("a", "a", &["ts"]))
.add_plugin(wasm_plugin("b", "b", &["ts", "vue"]));
})
.write_file("/file.ts", "")
.write_file("/file.vue", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert!(text.contains("a-1.0.0.wasm"), "{text}");
assert!(text.contains("b-1.0.0.wasm"), "{text}");
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_not_select_two_plugins_with_the_same_config_key() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info
.add_plugin(wasm_plugin("a", "markup", &["ts"]))
.add_plugin(wasm_plugin("b", "markup", &["ts", "vue"]));
})
.write_file("/file.ts", "")
.write_file("/file.vue", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert!(text.contains("a-1.0.0.wasm"), "{text}");
assert!(!text.contains("b-1.0.0.wasm"), "{text}");
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_map_picker_selection_back_when_display_is_reordered() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info.add_plugin(wasm_plugin("a", "a", &["js"])).add_plugin(wasm_plugin("b", "b", &["ts"]));
})
.write_file("/file.ts", "")
.build();
environment.set_multi_selection_result(vec![0, 1]); environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
let a_pos = text.find("a-1.0.0.wasm").expect("a present");
let b_pos = text.find("b-1.0.0.wasm").expect("b present");
assert!(a_pos < b_pos, "{text}");
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_select_keyless_plugins_for_distinct_extensions() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info
.add_plugin(TestInfoFilePlugin {
config_key: None,
..wasm_plugin("a", "", &["ts"])
})
.add_plugin(TestInfoFilePlugin {
config_key: None,
..wasm_plugin("b", "", &["vue"])
});
})
.write_file("/file.ts", "")
.write_file("/file.vue", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert!(text.contains("a-1.0.0.wasm"), "{text}");
assert!(text.contains("b-1.0.0.wasm"), "{text}");
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_not_select_plugins_with_shared_config_key_matched_via_config_items() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info.add_plugin(wasm_plugin("a", "shared", &["ts"])).add_plugin(TestInfoFilePlugin {
name: "b".to_string(),
version: "1.0.0".to_string(),
url: "https://plugins.dprint.dev/b-1.0.0.wasm".to_string(),
config_key: Some("shared".to_string()),
file_extensions: vec![],
config_excludes: vec![],
config_items: vec![TestInfoFileConfigItem {
file_match: TestInfoFileMatch {
file_extensions: vec!["rs".to_string()],
file_names: vec![],
},
config: serde_json::json!({}),
}],
..Default::default()
});
})
.write_file("/file.ts", "")
.write_file("/file.rs", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert!(text.contains("a-1.0.0.wasm"), "{text}");
assert!(!text.contains("b-1.0.0.wasm"), "{text}");
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
fn add_npm_package(builder: &mut TestEnvironmentBuilder, name: &str, file_name: &str) -> String {
let tarball = crate::test_helpers::create_test_npm_tarball(&[(&format!("package/{}", file_name), b"")]);
let checksum = crate::utils::get_sha256_checksum(&tarball);
builder
.add_remote_file_bytes(
&format!("https://registry.npmjs.org/{}", name),
serde_json::json!({
"dist-tags": { "latest": "1.1.0" },
"versions": { "1.1.0": { "dist": { "tarball": format!("https://registry.npmjs.org/{}/-/pkg-1.1.0.tgz", name) } } }
})
.to_string()
.into_bytes(),
)
.add_remote_file_bytes(&format!("https://registry.npmjs.org/{}/-/pkg-1.1.0.tgz", name), tarball);
checksum
}
#[test]
fn should_initialize_with_npm_specifiers() {
let mut builder = TestEnvironmentBuilder::new();
add_npm_package(&mut builder, "@dprint/a", "plugin.wasm");
let b_checksum = add_npm_package(&mut builder, "@dprint/b", "plugin.json");
let environment = builder
.with_info_file(|info| {
info
.add_plugin(TestInfoFilePlugin {
npm: Some(TestInfoFileNpm {
name: "@dprint/a".to_string(),
..Default::default()
}),
..wasm_plugin("a", "a", &["ts"])
})
.add_plugin(TestInfoFilePlugin {
name: "b".to_string(),
version: "1.0.0".to_string(),
url: "https://plugins.dprint.dev/b-1.0.0.json".to_string(),
file_extensions: vec!["vue".to_string()],
config_excludes: vec![],
checksum: Some("url-checksum".to_string()),
npm: Some(TestInfoFileNpm {
name: "@dprint/b".to_string(),
..Default::default()
}),
..Default::default()
})
.add_plugin(TestInfoFilePlugin {
name: "c".to_string(),
version: "1.0.0".to_string(),
url: "https://plugins.dprint.dev/c-1.0.0.json".to_string(),
file_extensions: vec!["rs".to_string()],
config_excludes: vec![],
checksum: Some("url-checksum".to_string()),
..Default::default()
});
})
.write_file("/file.ts", "")
.write_file("/file.vue", "")
.write_file("/file.rs", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
format!(
r#"{{
"a": {{
}},
"excludes": [],
"plugins": [
"npm:@dprint/a@1.1.0",
"npm:@dprint/b@1.1.0/plugin.json@{}",
"https://plugins.dprint.dev/c-1.0.0.json@url-checksum"
]
}}
"#,
b_checksum
)
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_fall_back_to_the_url_when_the_npm_registry_is_unreachable() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info.add_plugin(TestInfoFilePlugin {
npm: Some(TestInfoFileNpm {
name: "@dprint/a".to_string(),
..Default::default()
}),
..wasm_plugin("a", "a", &["ts"])
});
})
.write_file("/file.ts", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert!(text.contains("\"https://plugins.dprint.dev/a-1.0.0.wasm\""), "{text}");
let mut expected_messages = get_standard_logged_messages();
expected_messages.push("Failed resolving the npm package for a. Using its url instead. Failed to fetch npm packument for @dprint/a: Error downloading https://registry.npmjs.org/@dprint/a - 404 Not Found");
assert_eq!(environment.take_stderr_messages(), expected_messages);
});
}
#[test]
fn should_hold_an_npm_plugin_back_to_a_version_old_enough() {
let environment = age_test_builder("2099-12-31T00:00:00Z").build();
environment.set_fs_time(AGE_TEST_NOW);
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, age_test_options("P3D")).await.unwrap();
assert!(text.contains("\"npm:@dprint/a@1.0.0\""), "{text}");
let mut expected_messages = get_standard_logged_messages_no_plugin_selection();
expected_messages.push("Using @dprint/a 1.0.0 instead of 1.1.0, which is newer than the minimum dependency age allows (--minimum-dependency-age P3D). Pass --minimum-dependency-age=0 to use the latest version.");
assert_eq!(environment.take_stderr_messages(), expected_messages);
});
}
#[test]
fn should_error_when_no_npm_version_is_old_enough() {
let environment = age_test_builder("2099-12-31T00:00:00Z").build();
environment.set_fs_time(AGE_TEST_NOW);
environment.clone().run_in_runtime(async move {
let err = get_init_config_file_text(&environment, age_test_options("P400D")).await.err().unwrap();
assert_eq!(
err.to_string(),
"No version of @dprint/a at or below 1.1.0 is old enough for the minimum dependency age (--minimum-dependency-age P400D)."
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages_no_plugin_selection());
});
}
const AGE_TEST_NOW: u64 = 4102444800;
fn age_test_builder(latest_time: &str) -> TestEnvironmentBuilder {
let tarball_url = |version: &str| format!("https://registry.npmjs.org/@dprint/a/-/pkg-{}.tgz", version);
let mut builder = TestEnvironmentBuilder::new();
builder
.add_remote_file_bytes(
"https://registry.npmjs.org/@dprint/a",
serde_json::json!({
"dist-tags": { "latest": "1.1.0" },
"versions": {
"1.0.0": { "dist": { "tarball": tarball_url("1.0.0") } },
"1.1.0": { "dist": { "tarball": tarball_url("1.1.0") } },
},
"time": {
"1.0.0": "2099-01-01T00:00:00Z",
"1.1.0": latest_time,
}
})
.to_string()
.into_bytes(),
)
.with_info_file(|info| {
info.add_plugin(TestInfoFilePlugin {
npm: Some(TestInfoFileNpm {
name: "@dprint/a".to_string(),
..Default::default()
}),
..wasm_plugin("a", "a", &["ts"])
});
})
.write_file("/file.ts", "");
builder
}
fn age_test_options(age: &str) -> GetInitConfigFileTextOptions {
use std::str::FromStr;
GetInitConfigFileTextOptions {
non_interactive: true,
minimum_dependency_age: Some(MinimumDependencyAgeArg::from_str(age).unwrap()),
config_dir: None,
}
}
#[test]
fn should_get_default_initialization_text() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
for plugin in get_multi_plugins_config() {
info.add_plugin(plugin);
}
})
.write_file("/file.ts", "")
.write_file("/file.json", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"typescript": {
},
"json": {
},
"excludes": [
"**/something",
"**/*-asdf.json"
],
"plugins": [
"https://plugins.dprint.dev/typescript-0.17.2.wasm",
"https://plugins.dprint.dev/json-0.2.3.wasm"
]
}
"#
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_only_select_the_json_plugin_when_no_files_match() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
for plugin in get_multi_plugins_config() {
info.add_plugin(plugin);
}
})
.write_file("/readme.md", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"json": {
},
"excludes": [
"**/*-asdf.json"
],
"plugins": [
"https://plugins.dprint.dev/json-0.2.3.wasm"
]
}
"#
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_pre_select_plugin_by_file_name() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
for plugin in get_multi_plugins_config() {
info.add_plugin(plugin);
}
})
.write_file("/Cargo.toml", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"json": {
},
"excludes": [
"**/*-asdf.json",
"**/something",
"**other"
],
"plugins": [
"https://plugins.dprint.dev/json-0.2.3.wasm",
"https://plugins.dprint.dev/final-0.1.2.wasm"
]
}
"#
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_pre_select_process_plugin_by_extension() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
for plugin in get_multi_plugins_config() {
info.add_plugin(plugin);
}
})
.write_file("/file.ps", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"json": {
},
"excludes": [
"**/*-asdf.json"
],
"plugins": [
"https://plugins.dprint.dev/json-0.2.3.wasm",
"https://plugins.dprint.dev/process-0.1.0.json@test-checksum"
]
}
"#
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_scan_files_in_nested_directories() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
for plugin in get_multi_plugins_config() {
info.add_plugin(plugin);
}
})
.write_file("/src/nested/app.ts", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert!(text.contains("https://plugins.dprint.dev/typescript-0.17.2.wasm"), "{text}");
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_not_scan_ignored_directories() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
for plugin in get_multi_plugins_config() {
info.add_plugin(plugin);
}
})
.write_file("/node_modules/dep/app.ts", "")
.write_file("/.git/hooks/Cargo.toml", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"json": {
},
"excludes": [
"**/*-asdf.json"
],
"plugins": [
"https://plugins.dprint.dev/json-0.2.3.wasm"
]
}
"#
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_scan_files_in_hidden_directories() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
for plugin in get_multi_plugins_config() {
info.add_plugin(plugin);
}
})
.write_file("/.github/renovate.json", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert!(text.contains("https://plugins.dprint.dev/json-0.2.3.wasm"), "{text}");
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_not_scan_gitignored_files() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
for plugin in get_multi_plugins_config() {
info.add_plugin(plugin);
}
})
.write_file("/.gitignore", "generated/\nCargo.toml\n")
.write_file("/generated/app.ts", "")
.write_file("/Cargo.toml", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"json": {
},
"excludes": [
"**/*-asdf.json"
],
"plugins": [
"https://plugins.dprint.dev/json-0.2.3.wasm"
]
}
"#
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_match_extensions_case_insensitively() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
for plugin in get_multi_plugins_config() {
info.add_plugin(plugin);
}
})
.write_file("/MAIN.TS", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert!(text.contains("https://plugins.dprint.dev/typescript-0.17.2.wasm"), "{text}");
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_skip_prompt_when_non_interactive() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
for plugin in get_multi_plugins_config() {
info.add_plugin(plugin);
}
})
.write_file("/file.ts", "")
.build();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(
&environment,
GetInitConfigFileTextOptions {
non_interactive: true,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(
text,
r#"{
"typescript": {
},
"json": {
},
"excludes": [
"**/something",
"**/*-asdf.json"
],
"plugins": [
"https://plugins.dprint.dev/typescript-0.17.2.wasm",
"https://plugins.dprint.dev/json-0.2.3.wasm"
]
}
"#
);
assert_eq!(environment.take_stderr_messages(), Vec::<String>::new());
});
}
#[test]
fn should_get_initialization_text_when_can_access_url() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
for plugin in get_multi_plugins_config() {
info.add_plugin(plugin);
}
})
.build();
environment.set_multi_selection_result(vec![0, 1, 2]);
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"typescript": {
},
"json": {
},
"excludes": [
"**/something",
"**/*-asdf.json",
"**other"
],
"plugins": [
"https://plugins.dprint.dev/typescript-0.17.2.wasm",
"https://plugins.dprint.dev/json-0.2.3.wasm",
"https://plugins.dprint.dev/final-0.1.2.wasm"
]
}
"#
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_get_initialization_text_when_selecting_one_plugin() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
for plugin in get_multi_plugins_config() {
info.add_plugin(plugin);
}
})
.build();
environment.set_multi_selection_result(vec![0]);
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"json": {
},
"excludes": [
"**/*-asdf.json"
],
"plugins": [
"https://plugins.dprint.dev/json-0.2.3.wasm"
]
}
"#
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_get_initialization_text_when_selecting_no_plugins() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
for plugin in get_multi_plugins_config() {
info.add_plugin(plugin);
}
})
.build();
environment.set_multi_selection_result(vec![]);
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"excludes": [],
"plugins": [
// specify plugin urls here
]
}
"#
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_get_initialization_text_when_selecting_process_plugin() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
for plugin in get_multi_plugins_config() {
info.add_plugin(plugin);
}
})
.build();
environment.set_multi_selection_result(vec![3]);
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"excludes": [],
"plugins": [
"https://plugins.dprint.dev/process-0.1.0.json@test-checksum"
]
}
"#
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_get_initialization_text_when_cannot_access_url() {
let environment = TestEnvironment::new();
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"excludes": [
"**/*-lock.json"
],
"plugins": [
// specify plugin urls here
]
}
"#
);
let mut expected_messages = get_standard_logged_messages_no_plugin_selection();
expected_messages.push(concat!(
"There was a problem getting the latest plugin info, so the latest plugins may not be suggested. ",
"Error: Error downloading https://plugins.dprint.dev/info.json - 404 Not Found"
));
assert_eq!(environment.take_stderr_messages(), expected_messages);
});
}
#[test]
fn should_get_initialization_text_when_selecting_other_option() {
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!["test".to_string()],
..Default::default()
});
})
.build();
environment.set_selection_result(1);
environment.set_multi_selection_result(vec![0]);
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"typescript": {
},
"excludes": [
"test"
],
"plugins": [
"https://plugins.dprint.dev/typescript-0.17.2.wasm"
]
}
"#
);
assert_eq!(environment.take_stderr_messages(), get_standard_logged_messages());
});
}
#[test]
fn should_get_initialization_text_when_old_plugin_system() {
let environment = TestEnvironmentBuilder::new()
.with_info_file(|info| {
info.set_plugin_schema_version(999).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!["asdf".to_string()],
..Default::default()
});
})
.build();
environment.set_multi_selection_result(vec![0]);
environment.clone().run_in_runtime(async move {
let text = get_init_config_file_text(&environment, Default::default()).await.unwrap();
assert_eq!(
text,
r#"{
"excludes": [
"**/*-lock.json"
],
"plugins": [
// specify plugin urls here
]
}
"#
);
let mut expected_messages = get_standard_logged_messages_no_plugin_selection();
expected_messages.push(concat!(
"You are using an old version of dprint so the latest plugins may not be suggested. ",
"Consider upgrading to support new plugins. ",
"Plugin system schema version is 4, latest is 999."
));
assert_eq!(environment.take_stderr_messages(), expected_messages);
});
}
fn get_standard_logged_messages_no_plugin_selection() -> Vec<&'static str> {
vec![]
}
fn get_standard_logged_messages() -> Vec<&'static str> {
vec!["Select plugins (space to toggle, type to filter, enter to finish):"]
}
fn get_multi_plugins_config() -> Vec<TestInfoFilePlugin> {
vec![
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(), "tsx".to_string()],
config_excludes: vec!["**/something".to_string()],
..Default::default()
},
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!["**/*-asdf.json".to_string()],
..Default::default()
},
TestInfoFilePlugin {
name: "dprint-plugin-final".to_string(),
version: "0.1.2".to_string(),
url: "https://plugins.dprint.dev/final-0.1.2.wasm".to_string(),
file_names: Some(vec!["Cargo.toml".to_string()]),
file_extensions: vec!["tsx".to_string(), "rs".to_string()],
config_excludes: vec!["**/something".to_string(), "**other".to_string()],
..Default::default()
},
TestInfoFilePlugin {
name: "dprint-process-plugin".to_string(),
version: "0.1.0".to_string(),
url: "https://plugins.dprint.dev/process-0.1.0.json".to_string(),
file_extensions: vec!["ps".to_string()],
config_excludes: vec![],
checksum: Some("test-checksum".to_string()),
..Default::default()
},
]
}
}