1use std::path::{Path, PathBuf};
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5use skif_core::{FileSystem, OsFs};
6use skif_specifier::CONFIG_IDENTIFIERS;
7
8use crate::dependency_types::{DependencyType, DependencyTypes};
9use crate::error::ConfigError;
10use crate::overrides::ConfigOverrides;
11use crate::rules::{Level, RuleConfig, rules_map};
12use crate::selector::{Selector, anchor_patterns};
13
14#[cfg(feature = "schema")]
15use crate::schema::RuleConfigSchema;
16
17#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
19#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
20#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
21#[non_exhaustive]
22pub struct Config {
23 #[serde(skip_serializing_if = "is_false")]
26 pub root: bool,
27 #[serde(skip_serializing_if = "Vec::is_empty")]
29 pub extends: Vec<String>,
30 #[serde(skip_serializing_if = "Vec::is_empty")]
32 pub reset: Vec<ResetKey>,
33 pub workspace: Workspace,
35 pub lint: Lint,
37 #[serde(with = "rules_map")]
39 #[cfg_attr(feature = "schema", schemars(with = "IndexMap<String, RuleConfigSchema>"))]
40 pub rules: IndexMap<String, RuleConfig>,
41 pub ignore: Vec<IgnoreEntry>,
43 pub policy: Policy,
45 pub version_groups: Vec<VersionGroup>,
47 pub semver_groups: Vec<SemverGroup>,
49 #[serde(skip_serializing_if = "Vec::is_empty")]
51 pub custom_types: Vec<CustomType>,
52 pub catalog: Catalog,
54 pub workspace_protocol: WorkspaceProtocol,
56 pub dependency_families: Vec<DependencyFamily>,
58 pub update: Update,
60 #[serde(skip_serializing_if = "Vec::is_empty")]
62 pub update_groups: Vec<UpdateGroup>,
63 pub js: Js,
65 pub format: Format,
67 #[serde(skip_serializing_if = "Vec::is_empty")]
69 pub overrides: Vec<Override>,
70 #[serde(skip)]
73 #[cfg_attr(feature = "schema", schemars(skip))]
74 pub cli_levels: IndexMap<String, Level>,
75 #[serde(skip)]
78 #[cfg_attr(feature = "schema", schemars(skip))]
79 pub filter: InstanceFilter,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Default)]
86#[non_exhaustive]
87pub struct InstanceFilter {
88 pub dependencies: Selector,
90 pub dependency_types: DependencyTypes,
93 pub specifier_types: Vec<String>,
95 pub sources: Selector,
97}
98
99impl InstanceFilter {
100 pub fn new(
106 dependencies: &[String],
107 dependency_types: &[String],
108 specifier_types: &[String],
109 sources: &[String],
110 ) -> Result<Self, ConfigError> {
111 if let Some(message) = unknown_specifier_type(specifier_types) {
112 return Err(ConfigError::invalid(None, format!("--specifier-types: {message}")));
113 }
114 Ok(Self {
115 dependencies: Selector::new(dependencies.iter().cloned())?,
116 dependency_types: DependencyTypes::new(dependency_types)?,
117 specifier_types: specifier_types.to_vec(),
118 sources: Selector::new(sources.iter().cloned())?,
119 })
120 }
121
122 #[must_use]
124 pub fn is_empty(&self) -> bool {
125 self.dependencies.is_empty()
126 && self.dependency_types.is_empty()
127 && self.specifier_types.is_empty()
128 && self.sources.is_empty()
129 }
130
131 #[must_use]
133 pub fn unknown_custom_type<'a>(&'a self, config: &Config) -> Option<&'a str> {
134 self.dependency_types
135 .custom_names()
136 .find(|name| config.custom_types.iter().all(|custom| custom.name != *name))
137 }
138}
139
140#[allow(clippy::trivially_copy_pass_by_ref)]
141const fn is_false(value: &bool) -> bool {
142 !*value
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
147#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
148#[serde(rename_all = "kebab-case")]
149#[non_exhaustive]
150pub enum ResetKey {
151 VersionGroups,
153 SemverGroups,
155 Ignore,
157 DependencyFamilies,
159 CustomTypes,
161 Overrides,
163 UpdateGroups,
165}
166
167impl ResetKey {
168 #[must_use]
170 pub const fn name(self) -> &'static str {
171 match self {
172 Self::VersionGroups => "version-groups",
173 Self::SemverGroups => "semver-groups",
174 Self::Ignore => "ignore",
175 Self::DependencyFamilies => "dependency-families",
176 Self::CustomTypes => "custom-types",
177 Self::Overrides => "overrides",
178 Self::UpdateGroups => "update-groups",
179 }
180 }
181}
182
183fn selector_problem(patterns: &[String]) -> Option<String> {
185 match Selector::new(patterns.iter().cloned()) {
186 Err(ConfigError::Invalid { message, .. }) => Some(message),
187 _ => None,
188 }
189}
190
191impl Config {
192 pub fn from_toml_str(source: &str) -> Result<Self, ConfigError> {
198 Self::parse(source, None)
199 }
200
201 pub fn from_toml_file(path: &Path) -> Result<Self, ConfigError> {
206 Self::from_toml_file_with_fs(path, &OsFs)
207 }
208
209 pub fn from_toml_file_with_fs(path: &Path, fs: &dyn FileSystem) -> Result<Self, ConfigError> {
214 let source = fs.read_to_string(path).map_err(|source| ConfigError::Io {
215 path: path.to_path_buf(),
216 source,
217 })?;
218 Self::parse(&source, Some(path))
219 }
220
221 pub(crate) fn from_document(
226 document: toml::Spanned<toml::de::DeTable<'_>>,
227 source: &str,
228 path: Option<&Path>,
229 resolve_references: bool,
230 ) -> Result<Self, ConfigError> {
231 let config = Self::deserialize(toml::de::Deserializer::from(document))
232 .map_err(|err| ConfigError::from_toml(&err, source, path))?;
233 config.validate_with(path, resolve_references)?;
234 Ok(config)
235 }
236
237 fn parse(source: &str, path: Option<&Path>) -> Result<Self, ConfigError> {
240 let from_toml = |err: &toml::de::Error| ConfigError::from_toml(err, source, path);
241 let document = toml::de::DeTable::parse(source).map_err(|err| from_toml(&err))?;
242 let config = Self::from_document(document.clone(), source, path, true)?;
243 if !config.has_extend_keys() {
244 return Ok(config);
245 }
246 let table = toml::Table::deserialize(toml::de::Deserializer::from(document)).map_err(|err| from_toml(&err))?;
247 let mut merger = crate::merge::Merger::default();
248 merger.apply(0, &table, &std::collections::HashMap::new());
249 let folded = merger.into_table();
250 toml::Value::Table(folded)
251 .try_into()
252 .map_err(|err: toml::de::Error| ConfigError::invalid(path, err.message()))
253 }
254
255 fn has_extend_keys(&self) -> bool {
256 !self.workspace.extend_ignore_packages.is_empty()
257 || !self.js.extend_ignore.is_empty()
258 || !self.js.extend_ignore_dependencies.is_empty()
259 || !self.js.extend_ignore_binaries.is_empty()
260 || !self.js.extend_ignore_unresolved.is_empty()
261 || !self.catalog.extend_exclude.is_empty()
262 }
263
264 pub fn to_toml_string(&self) -> Result<String, toml::ser::Error> {
269 toml::to_string(self)
270 }
271
272 pub fn validate(&self) -> Result<(), ConfigError> {
277 self.validate_with(None, true)
278 }
279
280 fn validate_with(&self, path: Option<&Path>, resolve_references: bool) -> Result<(), ConfigError> {
281 let invalid = |message: String| Err(ConfigError::invalid(path, message));
282 for (index, group) in self.version_groups.iter().enumerate() {
283 if let Some(message) = group.inconsistency() {
284 return invalid(format!("version-groups[{index}]: {message}"));
285 }
286 if let Some(message) = unknown_specifier_type(&group.specifier_types) {
287 return invalid(format!("version-groups[{index}]: {message}"));
288 }
289 }
290 for (index, group) in self.semver_groups.iter().enumerate() {
291 if let Some(message) = group.inconsistency() {
292 return invalid(format!("semver-groups[{index}]: {message}"));
293 }
294 if let Some(message) = unknown_specifier_type(&group.specifier_types) {
295 return invalid(format!("semver-groups[{index}]: {message}"));
296 }
297 }
298 for (index, group) in self.update_groups.iter().enumerate() {
299 if let Some(message) = group.inconsistency() {
300 return invalid(format!("update-groups[{index}]: {message}"));
301 }
302 if let Some(message) = unknown_specifier_type(&group.specifier_types) {
303 return invalid(format!("update-groups[{index}]: {message}"));
304 }
305 }
306 if self.update.max_concurrent_requests == 0 {
307 return invalid("update.max-concurrent-requests must be at least 1".to_string());
308 }
309 if let Some(message) = self.custom_type_problem(resolve_references) {
310 return invalid(message);
311 }
312 for (index, entry) in self.ignore.iter().enumerate() {
313 if entry.rules.is_empty() && entry.packages.is_empty() && entry.dependencies.is_empty() {
314 return invalid(format!(
315 "ignore[{index}]: set at least one of `rules`, `packages` or `dependencies`; an entry without filters would suppress every diagnostic"
316 ));
317 }
318 if entry.reason.as_deref().is_some_and(|reason| reason.trim().is_empty()) {
319 return invalid(format!("ignore[{index}]: `reason` is empty; say why or remove it"));
320 }
321 }
322 if let Some(message) = self.format.inconsistency() {
323 return invalid(format!("format.{message}"));
324 }
325 for (index, scoped) in self.overrides.iter().enumerate() {
326 if let Some(message) = scoped.inconsistency() {
327 return invalid(format!("overrides[{index}]: {message}"));
328 }
329 }
330 if self.catalog.min_packages == 0 {
331 return invalid("catalog.min-packages must be at least 1".to_string());
332 }
333 if let Some(message) = selector_problem(&self.catalog.exclude) {
334 return invalid(format!("catalog.exclude: {message}"));
335 }
336 for (index, pattern) in self.workspace.discover_ignore.iter().enumerate() {
337 let glob = pattern.strip_prefix('!').unwrap_or(pattern).trim_start_matches('/');
339 let mut builder = ignore::gitignore::GitignoreBuilder::new("");
340 let checked = builder
341 .add_line(None, pattern)
342 .map(|_| ())
343 .map_err(|err| err.to_string())
344 .and_then(|()| globset::Glob::new(glob).map(|_| ()).map_err(|err| err.to_string()));
345 if let Err(err) = checked {
346 return invalid(format!("workspace.discover-ignore[{index}]: {err}"));
347 }
348 }
349 for (index, family) in self.dependency_families.iter().enumerate() {
350 if family.name.is_empty() {
351 return invalid(format!("dependency-families[{index}]: `name` must not be empty"));
352 }
353 if family.enabled && family.packages.is_empty() {
354 return invalid(format!("dependency-families[{index}]: `packages` must not be empty"));
355 }
356 if let Some(message) = selector_problem(&family.packages) {
357 return invalid(format!("dependency-families[{index}]: {message}"));
358 }
359 }
360 Ok(())
361 }
362
363 fn custom_type_problem(&self, resolve_references: bool) -> Option<String> {
366 for (index, custom) in self.custom_types.iter().enumerate() {
367 if let Some(message) = custom.inconsistency() {
368 return Some(format!("custom-types[{index}]: {message}"));
369 }
370 if self.custom_types[..index]
371 .iter()
372 .any(|earlier| earlier.name == custom.name)
373 {
374 return Some(format!(
375 "custom-types[{index}]: the name \"{}\" is defined twice",
376 custom.name
377 ));
378 }
379 }
380 if !resolve_references {
381 return None;
382 }
383 let lists = std::iter::once(("policy.dependency-types".to_owned(), &self.policy.dependency_types))
384 .chain(self.version_groups.iter().enumerate().map(|(index, group)| {
385 (
386 format!("version-groups[{index}].dependency-types"),
387 &group.dependency_types,
388 )
389 }))
390 .chain(self.semver_groups.iter().enumerate().map(|(index, group)| {
391 (
392 format!("semver-groups[{index}].dependency-types"),
393 &group.dependency_types,
394 )
395 }))
396 .chain(self.update_groups.iter().enumerate().map(|(index, group)| {
397 (
398 format!("update-groups[{index}].dependency-types"),
399 &group.dependency_types,
400 )
401 }));
402 for (place, types) in lists {
403 if let Some(name) = types
404 .custom_names()
405 .find(|name| self.custom_types.iter().all(|custom| custom.name != *name))
406 {
407 let hint = closest(
408 name,
409 &self
410 .custom_types
411 .iter()
412 .map(|custom| custom.name.as_str())
413 .chain(BUILTIN_TYPE_TOKENS)
414 .collect::<Vec<_>>(),
415 )
416 .map_or_else(String::new, |s| format!("; did you mean \"{s}\"?"));
417 return Some(format!(
418 "{place}: unknown dependency type \"{name}\"{hint} (expected {}, or the name of a `[[custom-types]]` entry)",
419 BUILTIN_TYPE_TOKENS.join(", ")
420 ));
421 }
422 }
423 None
424 }
425
426 pub fn validate_rules(&self, known: &[&str]) -> Result<(), ConfigError> {
431 let configured = self.rules.keys();
432 let ignored = self.ignore.iter().flat_map(|entry| entry.rules.iter());
433 let scoped = self.overrides.iter().flat_map(|scoped| {
434 scoped
435 .rules
436 .keys()
437 .chain(scoped.ignore.iter().flat_map(|entry| entry.rules.iter()))
438 });
439 for id in configured.chain(ignored).chain(scoped) {
440 if !known.contains(&id.as_str()) {
441 return Err(ConfigError::UnknownRule {
442 rule: id.clone(),
443 suggestion: closest(id, known),
444 });
445 }
446 }
447 Ok(())
448 }
449
450 pub fn validate_plugins(&self, known: &[&str]) -> Result<(), ConfigError> {
456 let workspaces = self
457 .js
458 .workspaces
459 .iter()
460 .map(|(dir, workspace)| (format!("js.workspaces.\"{dir}\".plugins"), &workspace.plugins));
461 let overrides = self
462 .overrides
463 .iter()
464 .enumerate()
465 .map(|(index, scoped)| (format!("overrides[{index}].js.plugins"), &scoped.js.plugins));
466 let tables = std::iter::once(("js.plugins".to_owned(), &self.js.plugins))
467 .chain(workspaces)
468 .chain(overrides);
469 for (key, table) in tables {
470 if let Some(id) = table.keys().find(|id| !known.contains(&id.as_str())) {
471 let hint = closest(id, known).map_or_else(String::new, |close| format!("; did you mean `{close}`?"));
472 return Err(ConfigError::invalid(
473 None,
474 format!("{key}.{id}: unknown plugin `{id}`{hint}"),
475 ));
476 }
477 }
478 Ok(())
479 }
480
481 #[must_use]
483 pub fn rule(&self, id: &str) -> Option<&RuleConfig> {
484 self.rules.get(id)
485 }
486
487 pub fn apply(&mut self, overrides: &ConfigOverrides) -> Result<(), ConfigError> {
493 let ignore_packages = Selector::new(overrides.ignore_packages.iter().cloned())?;
494 let ignore_dependencies = Selector::new(overrides.ignore_dependencies.iter().cloned())?;
495 for (id, level) in &overrides.rules {
496 self.rules.entry(id.clone()).or_default().severity = Some(*level);
497 self.cli_levels.insert(id.clone(), *level);
498 }
499 for id in &overrides.ignore_rules {
500 self.rules.entry(id.clone()).or_default().severity = Some(Level::Off);
501 self.cli_levels.insert(id.clone(), Level::Off);
502 }
503 if !ignore_packages.is_empty() {
504 self.workspace.ignore_packages = ignore_packages;
505 }
506 if !ignore_dependencies.is_empty() {
507 self.ignore.push(IgnoreEntry {
508 dependencies: ignore_dependencies,
509 reason: Some("--ignore-dependency".to_owned()),
510 ..IgnoreEntry::default()
511 });
512 }
513 if let Some(value) = overrides.fail_on_warnings {
514 self.lint.fail_on_warnings = value;
515 }
516 if let Some(value) = overrides.production {
517 self.js.production = value;
518 }
519 if overrides.no_js {
520 self.js.enabled = false;
521 }
522 Ok(())
523 }
524}
525
526const BUILTIN_TYPE_TOKENS: [&str; 8] = [
528 "prod",
529 "dev",
530 "peer",
531 "optional",
532 "local",
533 "resolutions",
534 "overrides",
535 "pnpm-overrides",
536];
537
538fn unknown_specifier_type(specifier_types: &[String]) -> Option<String> {
540 specifier_types.iter().find_map(|raw| {
541 let name = raw.strip_prefix('!').unwrap_or(raw);
542 if CONFIG_IDENTIFIERS.contains(&name) {
543 return None;
544 }
545 let hint = closest(name, &CONFIG_IDENTIFIERS).map_or_else(String::new, |s| format!("; did you mean \"{s}\"?"));
546 Some(format!(
547 "unknown specifier type \"{name}\"{hint} (expected one of: {})",
548 CONFIG_IDENTIFIERS.join(", ")
549 ))
550 })
551}
552
553fn closest(id: &str, known: &[&str]) -> Option<String> {
554 let limit = (id.chars().count() / 3).clamp(2, 4);
555 known
556 .iter()
557 .map(|candidate| (strsim::levenshtein(id, candidate), candidate))
558 .filter(|(distance, _)| *distance <= limit)
559 .min_by_key(|(distance, _)| *distance)
560 .map(|(_, candidate)| (*candidate).to_string())
561}
562
563#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
565#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
566#[serde(rename_all = "kebab-case")]
567#[non_exhaustive]
568pub enum PackageManager {
569 Npm,
571 Pnpm,
573 Yarn,
575 Bun,
577}
578
579pub const FIXTURE_DIRECTORIES: [&str; 2] = ["__fixtures__", "fixtures"];
584
585pub const DEFAULT_DISCOVER_IGNORE: [&str; 4] = ["__fixtures__", "fixtures", "__mocks__", "__snapshots__"];
588
589fn default_discover_ignore() -> Vec<String> {
590 DEFAULT_DISCOVER_IGNORE.iter().map(|name| (*name).to_owned()).collect()
591}
592
593#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
595#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
596#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
597#[non_exhaustive]
598pub struct Workspace {
599 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
601 pub ignore_packages: Selector,
602 #[serde(skip_serializing_if = "Option::is_none")]
604 pub package_manager: Option<PackageManager>,
605 pub discover_ignore: Vec<String>,
611 #[serde(skip_serializing_if = "Selector::is_empty")]
613 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
614 pub extend_ignore_packages: Selector,
615}
616
617impl Default for Workspace {
618 fn default() -> Self {
619 Self {
620 ignore_packages: Selector::default(),
621 extend_ignore_packages: Selector::default(),
622 package_manager: None,
623 discover_ignore: default_discover_ignore(),
624 }
625 }
626}
627
628impl Workspace {
629 #[must_use]
631 pub fn with_ignore_packages(mut self, ignore_packages: Selector) -> Self {
632 self.ignore_packages = ignore_packages;
633 self
634 }
635
636 #[must_use]
638 pub const fn with_package_manager(mut self, package_manager: PackageManager) -> Self {
639 self.package_manager = Some(package_manager);
640 self
641 }
642
643 #[must_use]
645 pub fn with_discover_ignore(mut self, discover_ignore: Vec<String>) -> Self {
646 self.discover_ignore = discover_ignore;
647 self
648 }
649}
650
651#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
653#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
654#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
655#[non_exhaustive]
656pub struct Lint {
657 pub fail_on_warnings: bool,
659 #[serde(skip_serializing_if = "Option::is_none")]
661 pub max_level: Option<Level>,
662}
663
664impl Lint {
665 #[must_use]
667 pub const fn with_fail_on_warnings(mut self, fail_on_warnings: bool) -> Self {
668 self.fail_on_warnings = fail_on_warnings;
669 self
670 }
671
672 #[must_use]
674 pub const fn with_max_level(mut self, max_level: Level) -> Self {
675 self.max_level = Some(max_level);
676 self
677 }
678}
679
680#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
683#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
684#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
685#[non_exhaustive]
686pub struct IgnoreEntry {
687 pub rules: Vec<String>,
689 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
691 pub packages: Selector,
692 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
694 pub dependencies: Selector,
695 #[serde(skip_serializing_if = "Option::is_none")]
697 pub reason: Option<String>,
698}
699
700impl IgnoreEntry {
701 #[must_use]
703 pub fn with_rules<I, S>(mut self, rules: I) -> Self
704 where
705 I: IntoIterator<Item = S>,
706 S: Into<String>,
707 {
708 self.rules = rules.into_iter().map(Into::into).collect();
709 self
710 }
711
712 #[must_use]
714 pub fn with_packages(mut self, packages: Selector) -> Self {
715 self.packages = packages;
716 self
717 }
718
719 #[must_use]
721 pub fn with_dependencies(mut self, dependencies: Selector) -> Self {
722 self.dependencies = dependencies;
723 self
724 }
725
726 #[must_use]
728 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
729 self.reason = Some(reason.into());
730 self
731 }
732}
733
734#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
736#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
737#[serde(rename_all = "kebab-case")]
738#[non_exhaustive]
739pub enum Prefer {
740 #[default]
742 Highest,
743 Lowest,
745}
746
747#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
749#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
750#[serde(rename_all = "kebab-case")]
751#[non_exhaustive]
752pub enum OverrideReading {
753 #[default]
756 Skif,
757 Syncpack,
761}
762
763#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
765#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
766#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
767#[non_exhaustive]
768pub struct Policy {
769 pub prefer: Prefer,
771 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
773 pub dependency_types: DependencyTypes,
774 pub override_reading: OverrideReading,
777}
778
779impl Policy {
780 #[must_use]
782 pub const fn with_prefer(mut self, prefer: Prefer) -> Self {
783 self.prefer = prefer;
784 self
785 }
786
787 #[must_use]
789 pub fn with_dependency_types(mut self, dependency_types: DependencyTypes) -> Self {
790 self.dependency_types = dependency_types;
791 self
792 }
793
794 #[must_use]
796 pub const fn with_override_reading(mut self, override_reading: OverrideReading) -> Self {
797 self.override_reading = override_reading;
798 self
799 }
800}
801
802#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
804#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
805#[serde(rename_all = "kebab-case")]
806#[non_exhaustive]
807pub enum VersionPolicy {
808 Pinned,
810 Banned,
812 Ignored,
814 SameRange,
816 SameMinor,
818 SnappedTo,
820 #[default]
822 PreferredSemver,
823 SemverRangeOnly,
825}
826
827#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
829#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
830#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
831#[non_exhaustive]
832pub struct VersionGroup {
833 #[serde(skip_serializing_if = "Option::is_none")]
835 pub label: Option<String>,
836 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
838 pub dependencies: Selector,
839 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
841 pub packages: Selector,
842 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
844 pub dependency_types: DependencyTypes,
845 pub specifier_types: Vec<String>,
847 pub policy: VersionPolicy,
849 #[serde(skip_serializing_if = "Option::is_none")]
851 pub pin: Option<String>,
852 pub snap_to: Vec<String>,
854 #[serde(skip_serializing_if = "Option::is_none")]
856 pub prefer: Option<Prefer>,
857}
858
859impl VersionGroup {
860 #[must_use]
862 pub fn with_label(mut self, label: impl Into<String>) -> Self {
863 self.label = Some(label.into());
864 self
865 }
866
867 #[must_use]
869 pub fn with_dependencies(mut self, dependencies: Selector) -> Self {
870 self.dependencies = dependencies;
871 self
872 }
873
874 #[must_use]
876 pub fn with_packages(mut self, packages: Selector) -> Self {
877 self.packages = packages;
878 self
879 }
880
881 #[must_use]
883 pub fn with_dependency_types(mut self, dependency_types: DependencyTypes) -> Self {
884 self.dependency_types = dependency_types;
885 self
886 }
887
888 #[must_use]
890 pub fn with_specifier_types<I, S>(mut self, specifier_types: I) -> Self
891 where
892 I: IntoIterator<Item = S>,
893 S: Into<String>,
894 {
895 self.specifier_types = specifier_types.into_iter().map(Into::into).collect();
896 self
897 }
898
899 #[must_use]
901 pub const fn with_policy(mut self, policy: VersionPolicy) -> Self {
902 self.policy = policy;
903 self
904 }
905
906 #[must_use]
908 pub fn with_pin(mut self, pin: impl Into<String>) -> Self {
909 self.pin = Some(pin.into());
910 self
911 }
912
913 #[must_use]
915 pub fn with_snap_to<I, S>(mut self, snap_to: I) -> Self
916 where
917 I: IntoIterator<Item = S>,
918 S: Into<String>,
919 {
920 self.snap_to = snap_to.into_iter().map(Into::into).collect();
921 self
922 }
923
924 #[must_use]
926 pub const fn with_prefer(mut self, prefer: Prefer) -> Self {
927 self.prefer = Some(prefer);
928 self
929 }
930
931 fn inconsistency(&self) -> Option<&'static str> {
932 match self.policy {
933 VersionPolicy::Pinned if self.pin.is_none() => return Some("policy `pinned` requires `pin`"),
934 VersionPolicy::SnappedTo if self.snap_to.is_empty() => {
935 return Some("policy `snapped-to` requires `snap-to`");
936 }
937 _ => {}
938 }
939 if self.pin.is_some() && self.policy != VersionPolicy::Pinned {
940 return Some("`pin` is only valid with policy `pinned`");
941 }
942 if !self.snap_to.is_empty() && self.policy != VersionPolicy::SnappedTo {
943 return Some("`snap-to` is only valid with policy `snapped-to`");
944 }
945 if self.prefer.is_some() && self.policy != VersionPolicy::PreferredSemver {
946 return Some("`prefer` is only valid with policy `preferred-semver`");
947 }
948 None
949 }
950}
951
952#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
954#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
955#[non_exhaustive]
956pub enum SemverRange {
957 #[serde(rename = "")]
959 Exact,
960 #[serde(rename = "*")]
962 Any,
963 #[serde(rename = "^")]
965 Caret,
966 #[serde(rename = "~")]
968 Tilde,
969 #[serde(rename = ">")]
971 Gt,
972 #[serde(rename = ">=")]
974 Gte,
975 #[serde(rename = "<")]
977 Lt,
978 #[serde(rename = "<=")]
980 Lte,
981}
982
983#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
986#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
987#[serde(rename_all = "kebab-case", deny_unknown_fields)]
988#[non_exhaustive]
989pub struct SemverGroup {
990 #[serde(default, skip_serializing_if = "Option::is_none")]
992 pub label: Option<String>,
993 #[serde(default)]
995 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
996 pub dependencies: Selector,
997 #[serde(default)]
999 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
1000 pub packages: Selector,
1001 #[serde(default)]
1003 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
1004 pub dependency_types: DependencyTypes,
1005 #[serde(default)]
1007 pub specifier_types: Vec<String>,
1008 #[serde(default, skip_serializing_if = "Option::is_none")]
1010 pub range: Option<SemverRange>,
1011 #[serde(default, skip_serializing_if = "is_false")]
1014 pub ignored: bool,
1015}
1016
1017impl SemverGroup {
1018 #[must_use]
1020 pub fn new(range: SemverRange) -> Self {
1021 Self {
1022 label: None,
1023 dependencies: Selector::default(),
1024 packages: Selector::default(),
1025 dependency_types: DependencyTypes::default(),
1026 specifier_types: Vec::new(),
1027 range: Some(range),
1028 ignored: false,
1029 }
1030 }
1031
1032 #[must_use]
1034 pub fn ignored() -> Self {
1035 Self {
1036 range: None,
1037 ignored: true,
1038 ..Self::new(SemverRange::Exact)
1039 }
1040 }
1041
1042 fn inconsistency(&self) -> Option<&'static str> {
1043 match (self.range, self.ignored) {
1044 (None, false) => Some("set `range`, or `ignored = true` to exempt the dependencies from a range"),
1045 (Some(_), true) => Some("`range` and `ignored = true` are mutually exclusive"),
1046 _ => None,
1047 }
1048 }
1049
1050 #[must_use]
1052 pub fn with_label(mut self, label: impl Into<String>) -> Self {
1053 self.label = Some(label.into());
1054 self
1055 }
1056
1057 #[must_use]
1059 pub fn with_dependencies(mut self, dependencies: Selector) -> Self {
1060 self.dependencies = dependencies;
1061 self
1062 }
1063
1064 #[must_use]
1066 pub fn with_packages(mut self, packages: Selector) -> Self {
1067 self.packages = packages;
1068 self
1069 }
1070
1071 #[must_use]
1073 pub fn with_dependency_types(mut self, dependency_types: DependencyTypes) -> Self {
1074 self.dependency_types = dependency_types;
1075 self
1076 }
1077
1078 #[must_use]
1080 pub fn with_specifier_types<I, S>(mut self, specifier_types: I) -> Self
1081 where
1082 I: IntoIterator<Item = S>,
1083 S: Into<String>,
1084 {
1085 self.specifier_types = specifier_types.into_iter().map(Into::into).collect();
1086 self
1087 }
1088}
1089
1090#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1092#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1093#[serde(rename_all = "kebab-case")]
1094#[non_exhaustive]
1095pub enum UpdateTarget {
1096 #[default]
1098 Latest,
1099 Minor,
1101 Patch,
1103}
1104
1105impl UpdateTarget {
1106 #[must_use]
1109 pub const fn stricter(self, other: Self) -> Self {
1110 match (self, other) {
1111 (Self::Patch, _) | (_, Self::Patch) => Self::Patch,
1112 (Self::Minor, _) | (_, Self::Minor) => Self::Minor,
1113 _ => Self::Latest,
1114 }
1115 }
1116
1117 #[must_use]
1119 pub const fn name(self) -> &'static str {
1120 match self {
1121 Self::Latest => "latest",
1122 Self::Minor => "minor",
1123 Self::Patch => "patch",
1124 }
1125 }
1126}
1127
1128pub const DEFAULT_MAX_CONCURRENT_REQUESTS: usize = 12;
1130
1131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1133#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1134#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
1135#[non_exhaustive]
1136pub struct Update {
1137 #[serde(skip_serializing_if = "Option::is_none")]
1140 pub minimum_release_age: Option<u64>,
1141 pub max_concurrent_requests: usize,
1143}
1144
1145impl Default for Update {
1146 fn default() -> Self {
1147 Self {
1148 minimum_release_age: None,
1149 max_concurrent_requests: DEFAULT_MAX_CONCURRENT_REQUESTS,
1150 }
1151 }
1152}
1153
1154impl Update {
1155 #[must_use]
1157 pub const fn with_minimum_release_age(mut self, minutes: Option<u64>) -> Self {
1158 self.minimum_release_age = minutes;
1159 self
1160 }
1161
1162 #[must_use]
1164 pub const fn with_max_concurrent_requests(mut self, requests: usize) -> Self {
1165 self.max_concurrent_requests = requests;
1166 self
1167 }
1168}
1169
1170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1173#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1174#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1175#[non_exhaustive]
1176pub struct UpdateGroup {
1177 #[serde(default, skip_serializing_if = "Option::is_none")]
1179 pub label: Option<String>,
1180 #[serde(default)]
1182 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
1183 pub dependencies: Selector,
1184 #[serde(default)]
1187 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
1188 pub packages: Selector,
1189 #[serde(default)]
1192 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
1193 pub dependency_types: DependencyTypes,
1194 #[serde(default)]
1196 pub specifier_types: Vec<String>,
1197 #[serde(default, skip_serializing_if = "Option::is_none")]
1200 pub target: Option<UpdateTarget>,
1201 #[serde(default, skip_serializing_if = "is_false")]
1203 pub ignored: bool,
1204}
1205
1206impl UpdateGroup {
1207 #[must_use]
1209 pub fn new(target: UpdateTarget) -> Self {
1210 Self {
1211 label: None,
1212 dependencies: Selector::default(),
1213 packages: Selector::default(),
1214 dependency_types: DependencyTypes::default(),
1215 specifier_types: Vec::new(),
1216 target: Some(target),
1217 ignored: false,
1218 }
1219 }
1220
1221 #[must_use]
1223 pub fn ignored() -> Self {
1224 Self {
1225 target: None,
1226 ignored: true,
1227 ..Self::new(UpdateTarget::Latest)
1228 }
1229 }
1230
1231 fn inconsistency(&self) -> Option<&'static str> {
1232 match (self.target, self.ignored) {
1233 (None, false) => Some("set `target`, or `ignored = true` to leave the dependencies alone"),
1234 (Some(_), true) => Some("`target` and `ignored = true` are mutually exclusive"),
1235 _ => None,
1236 }
1237 }
1238
1239 #[must_use]
1241 pub fn with_label(mut self, label: impl Into<String>) -> Self {
1242 self.label = Some(label.into());
1243 self
1244 }
1245
1246 #[must_use]
1248 pub fn with_dependencies(mut self, dependencies: Selector) -> Self {
1249 self.dependencies = dependencies;
1250 self
1251 }
1252
1253 #[must_use]
1255 pub fn with_packages(mut self, packages: Selector) -> Self {
1256 self.packages = packages;
1257 self
1258 }
1259
1260 #[must_use]
1262 pub fn with_dependency_types(mut self, dependency_types: DependencyTypes) -> Self {
1263 self.dependency_types = dependency_types;
1264 self
1265 }
1266
1267 #[must_use]
1269 pub fn with_specifier_types<I, S>(mut self, specifier_types: I) -> Self
1270 where
1271 I: IntoIterator<Item = S>,
1272 S: Into<String>,
1273 {
1274 self.specifier_types = specifier_types.into_iter().map(Into::into).collect();
1275 self
1276 }
1277}
1278
1279#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1281#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1282#[non_exhaustive]
1283pub enum CustomStrategy {
1284 #[serde(rename = "name@version")]
1286 NameAtVersion,
1287 #[serde(rename = "version")]
1289 Version,
1290 #[serde(rename = "versions-by-name", alias = "versionsByName")]
1292 VersionsByName,
1293 #[serde(rename = "name~version")]
1296 NameTildeVersion,
1297}
1298
1299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1302#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1303#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1304#[non_exhaustive]
1305pub struct CustomType {
1306 pub name: String,
1310 pub path: String,
1312 pub strategy: CustomStrategy,
1314 #[serde(default, skip_serializing_if = "Option::is_none")]
1316 pub name_path: Option<String>,
1317}
1318
1319impl CustomType {
1320 #[must_use]
1322 pub fn new(name: impl Into<String>, path: impl Into<String>, strategy: CustomStrategy) -> Self {
1323 Self {
1324 name: name.into(),
1325 path: path.into(),
1326 strategy,
1327 name_path: None,
1328 }
1329 }
1330
1331 #[must_use]
1333 pub fn with_name_path(mut self, name_path: impl Into<String>) -> Self {
1334 self.name_path = Some(name_path.into());
1335 self
1336 }
1337
1338 pub fn name_segments(&self) -> impl Iterator<Item = &str> {
1340 self.name_path.iter().flat_map(|path| path.split('.'))
1341 }
1342
1343 pub fn segments(&self) -> impl Iterator<Item = &str> {
1345 self.path.split('.')
1346 }
1347
1348 fn inconsistency(&self) -> Option<String> {
1349 if self.name.is_empty() || self.name.starts_with('!') {
1350 return Some("`name` must not be empty or start with `!`".to_owned());
1351 }
1352 if DependencyType::is_builtin_token(&self.name) {
1353 return Some(format!(
1354 "`name` \"{}\" is the name of a built-in dependency type",
1355 self.name
1356 ));
1357 }
1358 if self.segments().any(str::is_empty) {
1359 return Some(format!(
1360 "`path` \"{}\" must be a dotted path of non-empty keys",
1361 self.path
1362 ));
1363 }
1364 let takes_name_path = self.strategy == CustomStrategy::NameTildeVersion;
1365 match (takes_name_path, self.name_path.is_some()) {
1366 (true, false) => return Some("strategy `name~version` requires `name-path`".to_owned()),
1367 (false, true) => return Some("`name-path` is only valid with strategy `name~version`".to_owned()),
1368 _ => {}
1369 }
1370 if self.name_segments().any(str::is_empty) {
1371 return Some("`name-path` must be a dotted path of non-empty keys".to_owned());
1372 }
1373 None
1374 }
1375}
1376
1377#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1379#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1380#[serde(rename_all = "kebab-case")]
1381#[non_exhaustive]
1382pub enum CatalogMode {
1383 #[default]
1385 Off,
1386 Prefer,
1388 Enforce,
1390}
1391
1392#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1394#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1395#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
1396#[non_exhaustive]
1397pub struct Catalog {
1398 pub mode: CatalogMode,
1400 pub min_packages: usize,
1402 pub exclude: Vec<String>,
1404 pub report_unused_entries: bool,
1406 #[serde(skip_serializing_if = "Vec::is_empty")]
1408 pub extend_exclude: Vec<String>,
1409}
1410
1411impl Default for Catalog {
1412 fn default() -> Self {
1413 Self {
1414 mode: CatalogMode::Off,
1415 min_packages: 2,
1416 exclude: Vec::new(),
1417 report_unused_entries: false,
1418 extend_exclude: Vec::new(),
1419 }
1420 }
1421}
1422
1423impl Catalog {
1424 #[must_use]
1426 pub const fn with_mode(mut self, mode: CatalogMode) -> Self {
1427 self.mode = mode;
1428 self
1429 }
1430
1431 #[must_use]
1433 pub const fn with_min_packages(mut self, min_packages: usize) -> Self {
1434 self.min_packages = min_packages;
1435 self
1436 }
1437
1438 #[must_use]
1440 pub fn with_exclude<I, S>(mut self, exclude: I) -> Self
1441 where
1442 I: IntoIterator<Item = S>,
1443 S: Into<String>,
1444 {
1445 self.exclude = exclude.into_iter().map(Into::into).collect();
1446 self
1447 }
1448
1449 #[must_use]
1451 pub const fn with_report_unused_entries(mut self, report_unused_entries: bool) -> Self {
1452 self.report_unused_entries = report_unused_entries;
1453 self
1454 }
1455}
1456
1457#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
1459#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1460#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
1461#[non_exhaustive]
1462pub struct WorkspaceProtocol {
1463 pub internal: bool,
1465 pub allow_local_paths: bool,
1467}
1468
1469impl WorkspaceProtocol {
1470 #[must_use]
1472 pub const fn with_internal(mut self, internal: bool) -> Self {
1473 self.internal = internal;
1474 self
1475 }
1476
1477 #[must_use]
1479 pub const fn with_allow_local_paths(mut self, allow_local_paths: bool) -> Self {
1480 self.allow_local_paths = allow_local_paths;
1481 self
1482 }
1483}
1484
1485#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1487#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1488#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
1489#[non_exhaustive]
1490pub struct Format {
1491 pub sort_first: Vec<String>,
1493 pub sort_az: Vec<String>,
1496 pub sort_exports: Vec<String>,
1499 pub sort_packages: bool,
1501 pub format_bugs: bool,
1503 pub format_repository: bool,
1505 #[serde(skip_serializing_if = "Option::is_none")]
1508 pub indent: Option<String>,
1509}
1510
1511fn strings(items: &[&str]) -> Vec<String> {
1512 items.iter().map(|item| (*item).to_owned()).collect()
1513}
1514
1515impl Default for Format {
1516 fn default() -> Self {
1517 Self {
1518 sort_first: strings(&["name", "description", "version", "author"]),
1519 sort_az: strings(&[
1520 "bin",
1521 "contributors",
1522 "dependencies",
1523 "devDependencies",
1524 "keywords",
1525 "peerDependencies",
1526 "resolutions",
1527 "scripts",
1528 ]),
1529 sort_exports: strings(&["types", "default"]),
1530 sort_packages: true,
1531 format_bugs: false,
1532 format_repository: false,
1533 indent: None,
1534 }
1535 }
1536}
1537
1538impl Format {
1539 #[must_use]
1541 pub fn with_sort_first<I: IntoIterator<Item = S>, S: Into<String>>(mut self, keys: I) -> Self {
1542 self.sort_first = keys.into_iter().map(Into::into).collect();
1543 self
1544 }
1545
1546 #[must_use]
1548 pub fn with_sort_az<I: IntoIterator<Item = S>, S: Into<String>>(mut self, keys: I) -> Self {
1549 self.sort_az = keys.into_iter().map(Into::into).collect();
1550 self
1551 }
1552
1553 #[must_use]
1555 pub fn with_sort_exports<I: IntoIterator<Item = S>, S: Into<String>>(mut self, keys: I) -> Self {
1556 self.sort_exports = keys.into_iter().map(Into::into).collect();
1557 self
1558 }
1559
1560 #[must_use]
1562 pub const fn with_sort_packages(mut self, sort: bool) -> Self {
1563 self.sort_packages = sort;
1564 self
1565 }
1566
1567 #[must_use]
1569 pub const fn with_format_bugs(mut self, format: bool) -> Self {
1570 self.format_bugs = format;
1571 self
1572 }
1573
1574 #[must_use]
1576 pub const fn with_format_repository(mut self, format: bool) -> Self {
1577 self.format_repository = format;
1578 self
1579 }
1580
1581 #[must_use]
1583 pub fn with_indent(mut self, indent: impl Into<String>) -> Self {
1584 self.indent = Some(indent.into());
1585 self
1586 }
1587
1588 #[must_use]
1590 pub fn indent_text(&self) -> Option<String> {
1591 self.indent.as_ref().map(|indent| indent.replace("\\t", "\t"))
1592 }
1593
1594 fn inconsistency(&self) -> Option<String> {
1595 let indent = self.indent_text()?;
1596 (indent.is_empty() || indent.chars().any(|c| c != ' ' && c != '\t')).then(|| {
1597 format!(
1598 "indent: {:?} is not a run of spaces or tabs",
1599 self.indent.as_deref().unwrap_or_default()
1600 )
1601 })
1602 }
1603}
1604
1605#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1607#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1608#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1609#[non_exhaustive]
1610pub struct DependencyFamily {
1611 pub name: String,
1613 #[serde(default)]
1615 pub packages: Vec<String>,
1616 #[serde(default = "default_true")]
1618 pub enabled: bool,
1619}
1620
1621impl DependencyFamily {
1622 #[must_use]
1624 pub fn new<I, S>(name: impl Into<String>, packages: I) -> Self
1625 where
1626 I: IntoIterator<Item = S>,
1627 S: Into<String>,
1628 {
1629 Self {
1630 name: name.into(),
1631 packages: packages.into_iter().map(Into::into).collect(),
1632 enabled: true,
1633 }
1634 }
1635
1636 #[must_use]
1638 pub const fn with_enabled(mut self, enabled: bool) -> Self {
1639 self.enabled = enabled;
1640 self
1641 }
1642}
1643
1644const fn default_true() -> bool {
1645 true
1646}
1647
1648#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1650#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1651#[serde(rename_all = "lowercase")]
1652#[non_exhaustive]
1653pub enum JsDiscovery {
1654 #[default]
1658 Auto,
1659 Git,
1662 Walk,
1664}
1665
1666#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
1668#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1669#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
1670#[non_exhaustive]
1671pub struct JsCycles {
1672 pub allow: Vec<Vec<String>>,
1675 pub dynamic_imports: bool,
1678}
1679
1680#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1682#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1683#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
1684#[non_exhaustive]
1685#[allow(
1686 clippy::struct_excessive_bools,
1687 reason = "each bool is an independent switch of the analysis"
1688)]
1689pub struct Js {
1690 pub enabled: bool,
1692 pub entry: Vec<String>,
1694 pub project: Vec<String>,
1696 pub ignore: Vec<String>,
1698 pub ignore_files: Vec<String>,
1701 pub ignore_dependencies: Vec<String>,
1703 pub ignore_binaries: Vec<String>,
1705 pub ignore_global_binaries: bool,
1709 pub ignore_unresolved: Vec<String>,
1712 pub ignore_workspaces: Vec<String>,
1714 pub ignore_exports_used_in_file: IgnoreExportsUsedInFile,
1716 pub ignore_members: Vec<String>,
1718 pub production: bool,
1720 #[serde(skip)]
1725 #[cfg_attr(feature = "schema", schemars(skip))]
1726 pub isolate_workspaces: bool,
1727 #[serde(skip)]
1730 #[cfg_attr(feature = "schema", schemars(skip))]
1731 pub no_gitignore: bool,
1732 pub include_entry_exports: bool,
1734 #[serde(skip_serializing_if = "Option::is_none")]
1738 pub tsconfig: Option<String>,
1739 pub use_tsconfig_files: bool,
1742 pub include_fixtures: bool,
1745 pub discovery: JsDiscovery,
1750 pub cycles: JsCycles,
1752 pub workspaces: IndexMap<String, JsWorkspace>,
1754 pub plugins: IndexMap<String, PluginConfig>,
1756 #[serde(skip_serializing_if = "Vec::is_empty")]
1758 pub extend_ignore: Vec<String>,
1759 #[serde(skip_serializing_if = "Vec::is_empty")]
1761 pub extend_ignore_dependencies: Vec<String>,
1762 #[serde(skip_serializing_if = "Vec::is_empty")]
1764 pub extend_ignore_binaries: Vec<String>,
1765 #[serde(skip_serializing_if = "Vec::is_empty")]
1767 pub extend_ignore_unresolved: Vec<String>,
1768}
1769
1770impl Js {
1771 #[must_use]
1773 pub fn with_entry<I, S>(mut self, entry: I) -> Self
1774 where
1775 I: IntoIterator<Item = S>,
1776 S: Into<String>,
1777 {
1778 self.entry = entry.into_iter().map(Into::into).collect();
1779 self
1780 }
1781
1782 #[must_use]
1784 pub fn with_project<I, S>(mut self, project: I) -> Self
1785 where
1786 I: IntoIterator<Item = S>,
1787 S: Into<String>,
1788 {
1789 self.project = project.into_iter().map(Into::into).collect();
1790 self
1791 }
1792
1793 #[must_use]
1795 pub const fn with_enabled(mut self, enabled: bool) -> Self {
1796 self.enabled = enabled;
1797 self
1798 }
1799
1800 #[must_use]
1802 pub const fn with_production(mut self, production: bool) -> Self {
1803 self.production = production;
1804 self
1805 }
1806
1807 #[must_use]
1809 pub const fn with_isolate_workspaces(mut self, isolate: bool) -> Self {
1810 self.isolate_workspaces = isolate;
1811 self
1812 }
1813
1814 #[must_use]
1816 pub fn with_tsconfig(mut self, path: impl Into<String>) -> Self {
1817 self.tsconfig = Some(path.into());
1818 self
1819 }
1820
1821 #[must_use]
1824 pub const fn with_use_tsconfig_files(mut self, enabled: bool) -> Self {
1825 self.use_tsconfig_files = enabled;
1826 self
1827 }
1828
1829 #[must_use]
1831 pub fn with_plugin(mut self, name: impl Into<String>, plugin: PluginConfig) -> Self {
1832 self.plugins.insert(name.into(), plugin);
1833 self
1834 }
1835}
1836
1837impl Default for Js {
1838 fn default() -> Self {
1839 Self {
1840 enabled: true,
1841 entry: Vec::new(),
1842 project: Vec::new(),
1843 ignore: Vec::new(),
1844 ignore_files: Vec::new(),
1845 ignore_dependencies: Vec::new(),
1846 ignore_binaries: Vec::new(),
1847 ignore_global_binaries: true,
1848 ignore_unresolved: Vec::new(),
1849 ignore_workspaces: Vec::new(),
1850 ignore_exports_used_in_file: IgnoreExportsUsedInFile::default(),
1851 ignore_members: Vec::new(),
1852 production: false,
1853 isolate_workspaces: false,
1854 no_gitignore: false,
1855 include_entry_exports: false,
1856 tsconfig: None,
1857 use_tsconfig_files: false,
1858 include_fixtures: false,
1859 discovery: JsDiscovery::Auto,
1860 cycles: JsCycles::default(),
1861 workspaces: IndexMap::new(),
1862 plugins: IndexMap::new(),
1863 extend_ignore: Vec::new(),
1864 extend_ignore_dependencies: Vec::new(),
1865 extend_ignore_binaries: Vec::new(),
1866 extend_ignore_unresolved: Vec::new(),
1867 }
1868 }
1869}
1870
1871#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1876#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1877#[serde(untagged)]
1878#[non_exhaustive]
1879pub enum IgnoreExportsUsedInFile {
1880 All(bool),
1882 Kinds(ExportKinds),
1884}
1885
1886impl<'de> Deserialize<'de> for IgnoreExportsUsedInFile {
1887 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1888 struct Visitor;
1889
1890 impl<'de> serde::de::Visitor<'de> for Visitor {
1891 type Value = IgnoreExportsUsedInFile;
1892
1893 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1894 f.write_str("`true`, `false` or a table of export kinds")
1895 }
1896
1897 fn visit_bool<E: serde::de::Error>(self, value: bool) -> Result<Self::Value, E> {
1898 Ok(IgnoreExportsUsedInFile::All(value))
1899 }
1900
1901 fn visit_map<A: serde::de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
1902 ExportKinds::deserialize(serde::de::value::MapAccessDeserializer::new(map))
1903 .map(IgnoreExportsUsedInFile::Kinds)
1904 }
1905 }
1906
1907 deserializer.deserialize_any(Visitor)
1908 }
1909}
1910
1911impl Default for IgnoreExportsUsedInFile {
1912 fn default() -> Self {
1913 Self::All(false)
1914 }
1915}
1916
1917#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
1919#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1920#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
1921#[non_exhaustive]
1922#[allow(clippy::struct_excessive_bools)]
1923pub struct ExportKinds {
1924 pub variable: bool,
1926 pub function: bool,
1928 pub class: bool,
1930 #[serde(rename = "enum")]
1932 pub enumeration: bool,
1933 pub namespace: bool,
1935 #[serde(rename = "type")]
1937 pub type_alias: bool,
1938 pub interface: bool,
1940 pub member: bool,
1943}
1944
1945#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
1947#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1948#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
1949#[non_exhaustive]
1950pub struct JsWorkspace {
1951 pub entry: Vec<String>,
1953 pub project: Vec<String>,
1955 pub ignore: Vec<String>,
1957 pub ignore_files: Vec<String>,
1959 pub ignore_dependencies: Vec<String>,
1961 pub ignore_binaries: Vec<String>,
1963 pub ignore_unresolved: Vec<String>,
1965 #[serde(skip_serializing_if = "Option::is_none")]
1967 pub include_entry_exports: Option<bool>,
1968 #[serde(skip_serializing_if = "Option::is_none")]
1970 pub ignore_exports_used_in_file: Option<IgnoreExportsUsedInFile>,
1971 pub ignore_members: Vec<String>,
1973 pub paths: IndexMap<String, Vec<String>>,
1975 pub plugins: IndexMap<String, PluginConfig>,
1977}
1978
1979#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1981#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1982#[serde(untagged)]
1983#[non_exhaustive]
1984pub enum PluginConfig {
1985 Enabled(bool),
1987 Options(PluginOptions),
1989}
1990
1991#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
1993#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1994#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
1995#[non_exhaustive]
1996pub struct PluginOptions {
1997 #[serde(skip_serializing_if = "Option::is_none")]
1999 pub enabled: Option<bool>,
2000 pub config: Vec<String>,
2002 pub entry: Vec<String>,
2004 pub project: Vec<String>,
2006}
2007
2008impl PluginOptions {
2009 #[must_use]
2011 pub fn new() -> Self {
2012 Self::default()
2013 }
2014
2015 #[must_use]
2017 pub const fn with_enabled(mut self, enabled: bool) -> Self {
2018 self.enabled = Some(enabled);
2019 self
2020 }
2021
2022 #[must_use]
2024 pub fn with_config<I, S>(mut self, config: I) -> Self
2025 where
2026 I: IntoIterator<Item = S>,
2027 S: Into<String>,
2028 {
2029 self.config = config.into_iter().map(Into::into).collect();
2030 self
2031 }
2032
2033 #[must_use]
2035 pub fn with_entry<I, S>(mut self, entry: I) -> Self
2036 where
2037 I: IntoIterator<Item = S>,
2038 S: Into<String>,
2039 {
2040 self.entry = entry.into_iter().map(Into::into).collect();
2041 self
2042 }
2043
2044 #[must_use]
2046 pub fn with_project<I, S>(mut self, project: I) -> Self
2047 where
2048 I: IntoIterator<Item = S>,
2049 S: Into<String>,
2050 {
2051 self.project = project.into_iter().map(Into::into).collect();
2052 self
2053 }
2054}
2055
2056#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
2061#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2062#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
2063#[non_exhaustive]
2064pub struct PackageJs {
2065 #[serde(skip_serializing_if = "Option::is_none")]
2067 pub entry: Option<Vec<String>>,
2068 #[serde(skip_serializing_if = "Option::is_none")]
2070 pub project: Option<Vec<String>>,
2071 #[serde(skip_serializing_if = "Option::is_none")]
2073 pub ignore: Option<Vec<String>>,
2074 #[serde(skip_serializing_if = "Option::is_none")]
2076 pub ignore_dependencies: Option<Vec<String>>,
2077 #[serde(skip_serializing_if = "Option::is_none")]
2079 pub ignore_binaries: Option<Vec<String>>,
2080 #[serde(skip_serializing_if = "Vec::is_empty")]
2082 pub extend_ignore: Vec<String>,
2083 #[serde(skip_serializing_if = "Vec::is_empty")]
2085 pub extend_ignore_dependencies: Vec<String>,
2086 #[serde(skip_serializing_if = "Vec::is_empty")]
2088 pub extend_ignore_binaries: Vec<String>,
2089 #[serde(skip_serializing_if = "Option::is_none")]
2091 pub ignore_unresolved: Option<Vec<String>>,
2092 #[serde(skip_serializing_if = "Vec::is_empty")]
2094 pub extend_ignore_unresolved: Vec<String>,
2095 #[serde(skip_serializing_if = "IndexMap::is_empty")]
2097 pub plugins: IndexMap<String, PluginConfig>,
2098}
2099
2100impl PackageJs {
2101 #[must_use]
2103 pub fn is_empty(&self) -> bool {
2104 self == &Self::default()
2105 }
2106}
2107
2108#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
2119#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2120#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
2121#[non_exhaustive]
2122pub struct Override {
2123 #[serde(skip_serializing_if = "Selector::is_empty")]
2125 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
2126 pub packages: Selector,
2127 #[serde(skip_serializing_if = "Selector::is_empty")]
2129 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
2130 pub files: Selector,
2131 #[serde(skip_serializing_if = "Option::is_none")]
2133 pub max_level: Option<Level>,
2134 #[serde(with = "rules_map", skip_serializing_if = "IndexMap::is_empty")]
2136 #[cfg_attr(feature = "schema", schemars(with = "IndexMap<String, RuleConfigSchema>"))]
2137 pub rules: IndexMap<String, RuleConfig>,
2138 #[serde(skip_serializing_if = "Vec::is_empty")]
2140 pub ignore: Vec<IgnoreEntry>,
2141 #[serde(skip_serializing_if = "PackageJs::is_empty")]
2143 pub js: PackageJs,
2144 #[serde(skip)]
2146 pub scope: Option<OverrideScope>,
2147}
2148
2149impl Override {
2150 pub(crate) fn inconsistency(&self) -> Option<&'static str> {
2151 if self.packages.is_empty() && self.files.is_empty() {
2152 return Some(
2153 "set at least one of `packages` and `files`; an override without a selector would apply everywhere",
2154 );
2155 }
2156 if !self.js.is_empty() && (self.packages.is_empty() || !self.files.is_empty()) {
2157 return Some("`js` needs `packages` and cannot be combined with `files`");
2158 }
2159 if self.rules.values().any(|rule| !rule.options.is_empty()) {
2160 return Some("rule options are root-scoped; an override can only set rule levels");
2161 }
2162 None
2163 }
2164
2165 #[must_use]
2171 pub fn applies_to(&self, package: Option<(&str, &Path)>, file: Option<&Path>) -> bool {
2172 self.scope.as_ref().is_some_and(|scope| scope.applies_to(package, file))
2173 }
2174
2175 #[must_use]
2178 pub fn applies_to_slashed(&self, package: Option<(&str, &str)>, file: Option<&str>) -> bool {
2179 self.scope
2180 .as_ref()
2181 .is_some_and(|scope| scope.applies_to_slashed(package, file))
2182 }
2183}
2184
2185#[derive(Debug, Clone, PartialEq)]
2187#[non_exhaustive]
2188pub struct OverrideScope {
2189 pub declared_in: PathBuf,
2191 pub source: usize,
2193 has_packages: bool,
2194 has_files: bool,
2195 packages: Selector,
2196 files: Selector,
2197}
2198
2199impl OverrideScope {
2200 pub fn new(scoped: &Override, declared_in: &Path, source: usize, root: &Path) -> Result<Self, ConfigError> {
2205 Ok(Self {
2206 declared_in: declared_in.to_path_buf(),
2207 source,
2208 has_packages: !scoped.packages.is_empty(),
2209 has_files: !scoped.files.is_empty(),
2210 packages: anchor_patterns(scoped.packages.patterns(), declared_in, root, false)?,
2211 files: anchor_patterns(scoped.files.patterns(), declared_in, root, true)?,
2212 })
2213 }
2214
2215 fn applies_to(&self, package: Option<(&str, &Path)>, file: Option<&Path>) -> bool {
2216 let package = package.map(|(name, dir)| (name, skif_core::slash(dir)));
2217 let file = file.map(skif_core::slash);
2218 self.applies_to_slashed(
2219 package.as_ref().map(|(name, dir)| (*name, dir.as_str())),
2220 file.as_deref(),
2221 )
2222 }
2223
2224 fn applies_to_slashed(&self, package: Option<(&str, &str)>, file: Option<&str>) -> bool {
2225 let package_ok =
2226 !self.has_packages || package.is_some_and(|(name, dir)| self.packages.matches_any([name, dir], true));
2227 let file_ok = !self.has_files || file.is_some_and(|file| self.files.matches(file, false));
2228 package_ok && file_ok
2229 }
2230}