diff --git a/crates/turborepo-lib/src/framework.rs b/crates/turborepo-lib/src/framework.rs new file mode 100644 index 0000000000000..16149ef627e75 --- /dev/null +++ b/crates/turborepo-lib/src/framework.rs @@ -0,0 +1,286 @@ +use std::sync::OnceLock; + +use crate::package_graph::WorkspaceInfo; + +#[derive(Debug, PartialEq)] +enum Strategy { + All, + Some, +} + +#[derive(Debug, PartialEq)] +struct Matcher { + strategy: Strategy, + dependencies: Vec<&'static str>, +} + +#[derive(Debug, PartialEq)] +pub struct Framework { + slug: &'static str, + env_wildcards: Vec<&'static str>, + dependency_match: Matcher, +} + +static FRAMEWORKS: OnceLock<[Framework; 12]> = OnceLock::new(); + +fn get_frameworks() -> &'static [Framework] { + FRAMEWORKS.get_or_init(|| { + [ + Framework { + slug: "blitzjs", + env_wildcards: vec!["NEXT_PUBLIC_*"], + dependency_match: Matcher { + strategy: Strategy::All, + dependencies: vec!["blitz"], + }, + }, + Framework { + slug: "nextjs", + env_wildcards: vec!["NEXT_PUBLIC_*"], + dependency_match: Matcher { + strategy: Strategy::All, + dependencies: vec!["next"], + }, + }, + Framework { + slug: "gatsby", + env_wildcards: vec!["GATSBY_*"], + dependency_match: Matcher { + strategy: Strategy::All, + dependencies: vec!["gatsby"], + }, + }, + Framework { + slug: "astro", + env_wildcards: vec!["PUBLIC_*"], + dependency_match: Matcher { + strategy: Strategy::All, + dependencies: vec!["astro"], + }, + }, + Framework { + slug: "solidstart", + env_wildcards: vec!["VITE_*"], + dependency_match: Matcher { + strategy: Strategy::All, + dependencies: vec!["solid-js", "solid-start"], + }, + }, + Framework { + slug: "vue", + env_wildcards: vec!["VUE_APP_*"], + dependency_match: Matcher { + strategy: Strategy::All, + dependencies: vec!["@vue/cli-service"], + }, + }, + Framework { + slug: "sveltekit", + env_wildcards: vec!["VITE_*"], + dependency_match: Matcher { + strategy: Strategy::All, + dependencies: vec!["@sveltejs/kit"], + }, + }, + Framework { + slug: "create-react-app", + env_wildcards: vec!["REACT_APP_*"], + dependency_match: Matcher { + strategy: Strategy::Some, + dependencies: vec!["react-scripts", "react-dev-utils"], + }, + }, + Framework { + slug: "nuxtjs", + env_wildcards: vec!["NUXT_ENV_*"], + dependency_match: Matcher { + strategy: Strategy::Some, + dependencies: vec!["nuxt", "nuxt-edge", "nuxt3", "nuxt3-edge"], + }, + }, + Framework { + slug: "redwoodjs", + env_wildcards: vec!["REDWOOD_ENV_*"], + dependency_match: Matcher { + strategy: Strategy::All, + dependencies: vec!["@redwoodjs/core"], + }, + }, + Framework { + slug: "vite", + env_wildcards: vec!["VITE_*"], + dependency_match: Matcher { + strategy: Strategy::All, + dependencies: vec!["vite"], + }, + }, + Framework { + slug: "sanity", + env_wildcards: vec!["SANITY_STUDIO_*"], + dependency_match: Matcher { + strategy: Strategy::All, + dependencies: vec!["@sanity/cli"], + }, + }, + ] + }) +} + +impl Matcher { + pub fn test(&self, workspace: &WorkspaceInfo, is_monorepo: bool) -> bool { + // In the case where we're not in a monorepo, i.e. single package mode + // `unresolved_external_dependencies` is not populated. In which + // case we should check `dependencies` instead. + let deps = if is_monorepo { + workspace.unresolved_external_dependencies.as_ref() + } else { + workspace.package_json.dependencies.as_ref() + }; + + match self.strategy { + Strategy::All => self + .dependencies + .iter() + .all(|dep| deps.map_or(false, |deps| deps.contains_key(*dep))), + Strategy::Some => self + .dependencies + .iter() + .any(|dep| deps.map_or(false, |deps| deps.contains_key(*dep))), + } + } +} + +#[allow(dead_code)] +pub fn infer_framework(workspace: &WorkspaceInfo, is_monorepo: bool) -> Option<&'static Framework> { + let frameworks = get_frameworks(); + + frameworks + .iter() + .find(|framework| framework.dependency_match.test(workspace, is_monorepo)) +} + +#[cfg(test)] +mod tests { + use test_case::test_case; + + use crate::{ + framework::{get_frameworks, infer_framework, Framework}, + package_graph::WorkspaceInfo, + package_json::PackageJson, + }; + + fn get_framework_by_slug(slug: &str) -> &'static Framework { + get_frameworks() + .iter() + .find(|framework| framework.slug == slug) + .expect("framework not found") + } + + #[test_case(WorkspaceInfo::default(), None, true; "empty dependencies")] + #[test_case( + WorkspaceInfo { + unresolved_external_dependencies: Some( + vec![("blitz".to_string(), "*".to_string())].into_iter().collect() + ), + ..Default::default() + }, + Some(get_framework_by_slug("blitzjs")), + true; + "blitz" + )] + #[test_case( + WorkspaceInfo { + unresolved_external_dependencies: Some( + vec![("blitz", "*"), ("next", "*")] + .into_iter() + .map(|(s1, s2)| (s1.to_string(), s2.to_string())) + .collect() + ), + ..Default::default() + }, + Some(get_framework_by_slug("blitzjs")), + true; + "Order is preserved (returns blitz, not next)" + )] + #[test_case( + WorkspaceInfo { + unresolved_external_dependencies: Some( + vec![("next", "*")] + .into_iter() + .map(|(s1, s2)| (s1.to_string(), s2.to_string())) + .collect() + ), + ..Default::default() + }, + Some(get_framework_by_slug("nextjs")), + true; + "Finds next without blitz" + )] + #[test_case( + WorkspaceInfo { + unresolved_external_dependencies: Some( + vec![("solid-js", "*"), ("solid-start", "*")] + .into_iter() + .map(|(s1, s2)| (s1.to_string(), s2.to_string())) + .collect() + ), + ..Default::default() + }, + Some(get_framework_by_slug("solidstart")), + true; + "match all strategy works (solid)" + )] + #[test_case( + WorkspaceInfo { + unresolved_external_dependencies: Some( + vec![("nuxt3", "*")] + .into_iter() + .map(|(s1, s2)| (s1.to_string(), s2.to_string())) + .collect() + ), + ..Default::default() + }, + Some(get_framework_by_slug("nuxtjs")), + true; + "match some strategy works (nuxt)" + )] + #[test_case( + WorkspaceInfo { + unresolved_external_dependencies: Some( + vec![("react-scripts", "*")] + .into_iter() + .map(|(s1, s2)| (s1.to_string(), s2.to_string())) + .collect() + ), + ..Default::default() + }, + Some(get_framework_by_slug("create-react-app")), + true; + "match some strategy works (create-react-app)" + )] + #[test_case( + WorkspaceInfo { + package_json: PackageJson { + dependencies: Some( + vec![("next", "*")] + .into_iter() + .map(|(s1, s2)| (s1.to_string(), s2.to_string())) + .collect() + ), + ..Default::default() + }, + ..Default::default() + }, + Some(get_framework_by_slug("nextjs")), + false; + "Finds next in non-monorepo" + )] + fn test_infer_framework( + workspace_info: WorkspaceInfo, + expected: Option<&'static Framework>, + is_monorepo: bool, + ) { + let framework = infer_framework(&workspace_info, is_monorepo); + assert_eq!(framework, expected); + } +} diff --git a/crates/turborepo-lib/src/lib.rs b/crates/turborepo-lib/src/lib.rs index 9de7fbf0a81cd..c1ae4ae10b02a 100644 --- a/crates/turborepo-lib/src/lib.rs +++ b/crates/turborepo-lib/src/lib.rs @@ -12,6 +12,7 @@ mod config; mod daemon; mod engine; mod execution_state; +mod framework; pub(crate) mod globwatcher; pub mod graph; mod manager; diff --git a/crates/turborepo-lib/src/package_graph/builder.rs b/crates/turborepo-lib/src/package_graph/builder.rs index 42f4e229e8353..a03f7b7965160 100644 --- a/crates/turborepo-lib/src/package_graph/builder.rs +++ b/crates/turborepo-lib/src/package_graph/builder.rs @@ -1,5 +1,5 @@ use std::{ - collections::{HashMap, HashSet}, + collections::{BTreeMap, HashMap, HashSet}, fmt, }; @@ -11,8 +11,12 @@ use turbopath::{ }; use turborepo_lockfiles::Lockfile; -use super::{Package, PackageGraph, WorkspaceInfo, WorkspaceName, WorkspaceNode}; -use crate::{package_json::PackageJson, package_manager::PackageManager}; +use super::{PackageGraph, WorkspaceInfo, WorkspaceName, WorkspaceNode}; +use crate::{ + package_graph::{PackageName, PackageVersion}, + package_json::PackageJson, + package_manager::PackageManager, +}; pub struct PackageGraphBuilder<'a> { repo_root: &'a AbsoluteSystemPath, @@ -397,9 +401,7 @@ impl<'a> BuildState<'a, ResolvedLockfile> { .as_ref() .map(|deps| { deps.iter() - .map(|Package { name, version }| { - (name.to_string(), version.to_string()) - }) + .map(|(name, version)| (name.to_string(), version.to_string())) .collect() }) .unwrap_or_default(); @@ -447,7 +449,7 @@ impl<'a> BuildState<'a, ResolvedLockfile> { struct Dependencies { internal: HashSet, - external: HashSet, + external: BTreeMap, } impl Dependencies { @@ -462,7 +464,7 @@ impl Dependencies { .parent() .expect("package.json path should have parent"); let mut internal = HashSet::new(); - let mut external = HashSet::new(); + let mut external = BTreeMap::new(); let splitter = DependencySplitter { repo_root, workspace_dir, @@ -472,10 +474,7 @@ impl Dependencies { if let Some(workspace) = splitter.is_internal(name, version) { internal.insert(workspace); } else { - external.insert(Package { - name: name.clone(), - version: version.clone(), - }); + external.insert(name.clone(), version.clone()); } } Self { internal, external } diff --git a/crates/turborepo-lib/src/package_graph/mod.rs b/crates/turborepo-lib/src/package_graph/mod.rs index 91d639081574e..d332390993b55 100644 --- a/crates/turborepo-lib/src/package_graph/mod.rs +++ b/crates/turborepo-lib/src/package_graph/mod.rs @@ -1,5 +1,5 @@ use std::{ - collections::{HashMap, HashSet}, + collections::{BTreeMap, HashMap, HashSet}, fmt, }; @@ -28,7 +28,7 @@ pub struct PackageGraph { pub struct WorkspaceInfo { pub package_json: PackageJson, pub package_json_path: AnchoredSystemPathBuf, - pub unresolved_external_dependencies: Option>, + pub unresolved_external_dependencies: Option>, pub transitive_dependencies: Option>, } @@ -38,11 +38,8 @@ impl WorkspaceInfo { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] -pub struct Package { - name: String, - version: String, -} +type PackageName = String; +type PackageVersion = String; /// Name of workspaces with a special marker for the workspace root #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] @@ -303,10 +300,9 @@ mod test { .unresolved_external_dependencies .as_ref() .unwrap(); - assert!(b_external.contains(&Package { - name: "c".into(), - version: "1.2.3".into() - })); + + let pkg_version = b_external.get("c").unwrap(); + assert_eq!(pkg_version, "1.2.3"); } struct MockLockfile {}