use std::path::Path;
use self::ConfigMatch::{
AllOf, Exact, Extension, FileToken, NodeDependency, NodeRuntimeDependency, Prefix,
PrefixWithNodeDependency,
};
use super::files::ProjectFiles;
use super::python;
use crate::ecosystem::Ecosystem as E;
use crate::{StackLabel, labels};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConfigMatch {
Exact(&'static str),
Prefix(&'static str),
AllOf(&'static [&'static str]),
Extension(&'static str),
NodeDependency(&'static [&'static str]),
PrefixWithNodeDependency(&'static str, &'static [&'static str]),
NodeRuntimeDependency(&'static [&'static str]),
FileToken(&'static str, &'static str),
}
pub type ConfigRule = (ConfigMatch, StackLabel, E);
pub const CONFIG_RULES: &[ConfigRule] = &[
(Prefix("next.config"), labels::NEXT_JS, E::Node),
(Prefix("nuxt.config"), labels::NUXT, E::Node),
(Exact("angular.json"), labels::ANGULAR, E::Node),
(
PrefixWithNodeDependency("svelte.config", &["@sveltejs/kit"]),
labels::SVELTEKIT,
E::Node,
),
(Prefix("astro.config"), labels::ASTRO, E::Node),
(Prefix("react-router.config"), labels::REACT_ROUTER, E::Node),
(Prefix("remix.config"), labels::REMIX, E::Node),
(NodeDependency(REMIX_PACKAGES), labels::REMIX, E::Node),
(Exact("nest-cli.json"), labels::NESTJS, E::Node),
(NodeDependency(&["@nestjs/core"]), labels::NESTJS, E::Node),
(NodeDependency(&["next"]), labels::NEXT_JS, E::Node),
(Prefix("vite.config"), labels::VITE, E::Node),
(Prefix("gatsby-config"), labels::GATSBY, E::Node),
(Prefix("vue.config"), labels::VUE_CLI, E::Node),
(Prefix("webpack.config"), labels::WEBPACK, E::Node),
(Exact("Cargo.toml"), labels::RUST, E::Rust),
(Exact("go.mod"), labels::GO, E::Go),
(Exact("go.work"), labels::GO, E::Go),
(
FileToken("pom.xml", SPRING_BOOT_GROUP),
labels::SPRING_BOOT,
E::Jvm,
),
(
FileToken("build.gradle.kts", SPRING_BOOT_GROUP),
labels::SPRING_BOOT,
E::Jvm,
),
(
FileToken("build.gradle", SPRING_BOOT_GROUP),
labels::SPRING_BOOT,
E::Jvm,
),
(Exact("pom.xml"), labels::JAVA_MAVEN, E::Jvm),
(Exact("build.gradle.kts"), labels::KOTLIN_GRADLE, E::Jvm),
(Exact("build.gradle"), labels::JAVA_GRADLE, E::Jvm),
(Exact("settings.gradle.kts"), labels::KOTLIN_GRADLE, E::Jvm),
(Exact("settings.gradle"), labels::JAVA_GRADLE, E::Jvm),
(
AllOf(&["artisan", "composer.json"]),
labels::LARAVEL,
E::Php,
),
(
AllOf(&["composer.json", "symfony.lock"]),
labels::SYMFONY,
E::Php,
),
(
AllOf(&["composer.json", "bin/console"]),
labels::SYMFONY,
E::Php,
),
(Exact("composer.json"), labels::PHP, E::Php),
(FileToken("mix.exs", ":phoenix"), labels::PHOENIX, E::Beam),
(Exact("mix.exs"), labels::ELIXIR, E::Beam),
(Exact("deno.json"), labels::DENO, E::Deno),
(Exact("deno.jsonc"), labels::DENO, E::Deno),
];
const SPRING_BOOT_GROUP: &str = "org.springframework.boot";
const REMIX_PACKAGES: &[&str] = &[
"@remix-run/dev",
"@remix-run/react",
"@remix-run/node",
"@remix-run/serve",
"@remix-run/cloudflare",
];
pub const LATE_CONFIG_RULES: &[ConfigRule] = &[
(
AllOf(&["Gemfile", "config.ru", "bin/rails"]),
labels::RAILS,
E::Ruby,
),
(AllOf(&["Gemfile", "config.ru"]), labels::RUBY_RACK, E::Ruby),
(Extension("csproj"), labels::DOTNET, E::DotNet),
(Extension("fsproj"), labels::DOTNET_FSHARP, E::DotNet),
(Extension("sln"), labels::DOTNET, E::DotNet),
(Extension("slnx"), labels::DOTNET, E::DotNet),
(
NodeRuntimeDependency(&["express"]),
labels::EXPRESS,
E::Node,
),
];
#[must_use]
pub fn detect_from_config(project_root: &Path) -> Option<StackLabel> {
detect_for_scope(project_root, ConfigScope::All)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConfigScope {
All,
CompiledFirst,
NodeFirst,
Ecosystem(E),
}
pub fn detect_for_scope(project_root: &Path, scope: ConfigScope) -> Option<StackLabel> {
let files = ProjectFiles::read(project_root)?;
match scope {
ConfigScope::All => detect_with(project_root, &files, |_| true, false),
ConfigScope::CompiledFirst => detect_with(project_root, &files, E::is_compiled, false)
.or_else(|| detect_with(project_root, &files, |_| true, false)),
ConfigScope::NodeFirst => detect_with(project_root, &files, |rule| rule == E::Node, false)
.or_else(|| detect_with(project_root, &files, |_| true, false)),
ConfigScope::Ecosystem(process) => detect_with(
project_root,
&files,
|rule| process.accepts_config(rule),
process == E::Python,
),
}
}
fn detect_with(
project_root: &Path,
files: &ProjectFiles,
in_scope: impl Fn(E) -> bool + Copy,
python_process: bool,
) -> Option<StackLabel> {
detect_from_rules(project_root, files, CONFIG_RULES, in_scope)
.or_else(|| {
in_scope(E::Python)
.then(|| python::detect_python_project(project_root, files, python_process))
.flatten()
})
.or_else(|| detect_from_rules(project_root, files, LATE_CONFIG_RULES, in_scope))
}
fn detect_from_rules(
project_root: &Path,
files: &ProjectFiles,
rules: &[ConfigRule],
in_scope: impl Fn(E) -> bool,
) -> Option<StackLabel> {
rules
.iter()
.filter(|(_, _, ecosystem)| in_scope(*ecosystem))
.find(|(matcher, _, _)| rule_matches(project_root, files, *matcher))
.map(|(_, label, _)| label.clone())
}
fn rule_matches(project_root: &Path, files: &ProjectFiles, matcher: ConfigMatch) -> bool {
match matcher {
Exact(name) => files.contains_exact(name),
Prefix(prefix) => files.contains_prefix(prefix),
AllOf(paths) => paths
.iter()
.all(|path| files.contains_path(project_root, path)),
Extension(extension) => files.contains_extension(extension),
NodeDependency(packages) => files.node_dependencies(project_root).has_any(packages),
NodeRuntimeDependency(packages) => {
files.node_dependencies(project_root).has_runtime(packages)
}
PrefixWithNodeDependency(prefix, packages) => {
files.contains_prefix(prefix) && files.node_dependencies(project_root).has_any(packages)
}
FileToken(file_name, token) => files.mentions_token(project_root, file_name, token),
}
}