use std::path::PathBuf;
use std::sync::OnceLock;
use skif_core::FileSystem;
pub use skif_core::{IncompleteCause, IncompleteEntries, IncompleteScope};
use skif_workspace::{Package, Workspace};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathAlias {
pub find: String,
pub target: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct RunBinary {
pub name: String,
pub file: PathBuf,
pub dir: Option<String>,
pub filtered: bool,
}
impl RunBinary {
#[must_use]
pub fn new(name: String, file: PathBuf, dir: Option<String>, filtered: bool) -> Self {
Self {
name,
file,
dir,
filtered,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigDependency {
pub name: String,
pub file: PathBuf,
pub type_only: bool,
pub reader_may_list: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BuildDir {
pub out_dir: String,
pub root_dir: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomElementLibrary {
pub modules: Vec<String>,
pub decorator: String,
pub base_class: Option<String>,
}
impl CustomElementLibrary {
#[must_use]
pub fn is_module(&self, specifier: &str) -> bool {
self.modules
.iter()
.any(|module| module == specifier || (module.ends_with('/') && specifier.starts_with(module.as_str())))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct WorkspaceInputs {
pub entry: Vec<String>,
pub production_entry: Vec<String>,
pub entry_regex: Vec<String>,
pub scoped_entry_regex: Vec<ScopedEntryRegex>,
pub project: Vec<String>,
pub ignore: Vec<String>,
pub ignore_dependencies: Vec<String>,
pub ignore_binaries: Vec<String>,
pub virtual_modules: Vec<String>,
pub dependencies: Vec<String>,
pub config_dependencies: Vec<ConfigDependency>,
pub binaries: Vec<String>,
pub run_binaries: Vec<RunBinary>,
pub config_files: Vec<PathBuf>,
pub aliases: Vec<PathAlias>,
pub build_dirs: Vec<BuildDir>,
pub conditions: Vec<String>,
pub module_dirs: Vec<String>,
pub extensions: Vec<String>,
pub custom_elements: Vec<CustomElementLibrary>,
pub entries_incomplete: bool,
pub incomplete_reasons: Vec<IncompleteEntries>,
pub incomplete_scope: IncompleteScope,
}
impl WorkspaceInputs {
pub fn extend(&mut self, other: Self) {
self.entry.extend(other.entry);
self.production_entry.extend(other.production_entry);
self.entry_regex.extend(other.entry_regex);
self.scoped_entry_regex.extend(other.scoped_entry_regex);
self.project.extend(other.project);
self.ignore.extend(other.ignore);
self.ignore_dependencies.extend(other.ignore_dependencies);
self.ignore_binaries.extend(other.ignore_binaries);
self.virtual_modules.extend(other.virtual_modules);
self.dependencies.extend(other.dependencies);
self.config_dependencies.extend(other.config_dependencies);
self.binaries.extend(other.binaries);
self.run_binaries.extend(other.run_binaries);
self.config_files.extend(other.config_files);
self.aliases.extend(other.aliases);
self.build_dirs.extend(other.build_dirs);
self.conditions.extend(other.conditions);
self.module_dirs.extend(other.module_dirs);
self.extensions.extend(other.extensions);
self.custom_elements.extend(other.custom_elements);
if other.entries_incomplete {
self.incomplete_scope = if self.entries_incomplete {
std::mem::take(&mut self.incomplete_scope).union(other.incomplete_scope)
} else {
other.incomplete_scope
};
}
self.entries_incomplete |= other.entries_incomplete;
self.incomplete_reasons.extend(other.incomplete_reasons);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScopedEntryRegex {
pub scope: String,
pub regex: String,
}
#[derive(Debug)]
pub struct WorkspaceListing {
gitignore: bool,
sequential: bool,
nx_projects: OnceLock<Vec<(String, String)>>,
}
impl WorkspaceListing {
#[must_use]
pub fn new(gitignore: bool, sequential: bool) -> Self {
Self {
gitignore,
sequential,
nx_projects: OnceLock::new(),
}
}
pub fn nx_projects(&self, fs: &dyn FileSystem, workspace: &Workspace) -> &[(String, String)] {
skif_core::get_or_compute(&self.nx_projects, || {
crate::discover::nx_project_dirs(fs, workspace, self.sequential, self.gitignore)
})
}
}
#[derive(Clone, Copy)]
pub struct InputRequest<'a> {
pub workspace: &'a Workspace,
pub package: &'a Package,
pub fs: &'a dyn FileSystem,
pub ancestor_dependencies: &'a [String],
pub workspace_key: &'a str,
pub listing: &'a WorkspaceListing,
}
impl std::fmt::Debug for InputRequest<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InputRequest")
.field("package", &self.package.display_name())
.finish_non_exhaustive()
}
}
pub trait InputSource: Send + Sync {
fn inputs(&self, request: &InputRequest<'_>) -> WorkspaceInputs;
}
const _: () = {
fn assert_send_sync<T: Send + Sync>() {}
let _ = assert_send_sync::<WorkspaceInputs>;
};
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use std::io;
use std::path::Path;
use std::sync::{Arc, Mutex};
use skif_config::Config;
use skif_core::{DirEntry, MemoryFs};
use super::*;
struct Counting {
inner: MemoryFs,
listed: Mutex<Vec<PathBuf>>,
}
impl FileSystem for Counting {
fn read_to_string(&self, path: &Path) -> io::Result<Arc<str>> {
self.inner.read_to_string(path)
}
fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
self.listed.lock().unwrap().push(path.to_path_buf());
self.inner.read_dir(path)
}
fn is_file(&self, path: &Path) -> bool {
self.inner.is_file(path)
}
fn is_dir(&self, path: &Path) -> bool {
self.inner.is_dir(path)
}
}
#[test]
fn the_nx_projects_are_listed_once_per_run() {
let memory = MemoryFs::new()
.with_file("/ws/package.json", r#"{"name":"r","workspaces":["packages/*"]}"#)
.with_file("/ws/apps/web/project.json", "{}")
.with_file("/ws/packages/b/package.json", r#"{"name":"b"}"#)
.with_file("/ws/packages/b/project.json", "{}");
let config = Config::from_toml_str("").unwrap();
let workspace = Workspace::discover_with_fs(Path::new("/ws"), &config, &memory).unwrap();
let fs = Counting {
inner: memory,
listed: Mutex::new(Vec::new()),
};
let listing = WorkspaceListing::new(true, true);
let expected = [
("apps/web".to_owned(), String::new()),
("packages/b".to_owned(), "packages/b".to_owned()),
];
assert_eq!(listing.nx_projects(&fs, &workspace), expected);
let first_walk = fs.listed.lock().unwrap().clone();
assert_eq!(listing.nx_projects(&fs, &workspace), expected);
let listed = fs.listed.lock().unwrap().clone();
assert_eq!(listed, first_walk);
let apps = listed.iter().filter(|dir| *dir == Path::new("/ws/apps")).count();
assert_eq!(apps, 1, "{listed:?}");
}
}