diff --git a/Cargo.lock b/Cargo.lock index f04c5aae5..e7304fc69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,6 +147,7 @@ dependencies = [ "lazy_static", "md-5", "pathdiff", + "pico", "prelude", "serde", "serde_json", diff --git a/crates/artifact_content/Cargo.toml b/crates/artifact_content/Cargo.toml index 5c03c6077..b90fd39a5 100644 --- a/crates/artifact_content/Cargo.toml +++ b/crates/artifact_content/Cargo.toml @@ -10,6 +10,7 @@ graphql_lang_types = { path = "../graphql_lang_types" } intern = { path = "../../relay-crates/intern" } isograph_schema = { path = "../isograph_schema" } isograph_config = { path = "../isograph_config" } +pico = { path = "../pico" } isograph_lang_types = { path = "../isograph_lang_types" } prelude = { path = "../prelude" } indexmap = { workspace = true } diff --git a/crates/artifact_content/src/file_system_state.rs b/crates/artifact_content/src/file_system_state.rs new file mode 100644 index 000000000..03b127172 --- /dev/null +++ b/crates/artifact_content/src/file_system_state.rs @@ -0,0 +1,513 @@ +use pico::Index; +use std::{ + collections::{HashMap, HashSet}, + path::Path, +}; + +use crate::operation_text::hash; +use common_lang_types::{ + ArtifactFileName, ArtifactHash, ArtifactPathAndContent, FileContent, FileSystemOperation, + SelectableName, ServerObjectEntityName, +}; +use isograph_config::PersistedDocumentsHashAlgorithm; + +#[derive(Debug, Clone, Default)] +#[expect(clippy::type_complexity)] +pub struct FileSystemState { + root_files: HashMap, ArtifactHash)>, + nested_files: HashMap< + ServerObjectEntityName, + HashMap, ArtifactHash)>>, + >, +} + +impl FileSystemState { + pub fn recreate_all(state: &Self, artifact_directory: &Path) -> Vec { + let mut operations: Vec = Vec::new(); + operations.push(FileSystemOperation::DeleteDirectory( + artifact_directory.to_path_buf(), + )); + + for (new_server_object_entity_name, new_selectable_map) in &state.nested_files { + let new_server_object_path = artifact_directory.join(new_server_object_entity_name); + + for (new_selectable, new_files) in new_selectable_map { + let new_selectable_path = new_server_object_path.join(new_selectable); + operations.push(FileSystemOperation::CreateDirectory( + new_selectable_path.clone(), + )); + + for (new_file_name, (new_index, _)) in new_files { + let new_file_path = new_selectable_path.join(new_file_name); + operations.push(FileSystemOperation::WriteFile( + new_file_path, + new_index.clone(), + )); + } + } + } + + for (new_file_name, (new_index, _)) in &state.root_files { + let new_file_path = artifact_directory.join(new_file_name); + operations.push(FileSystemOperation::WriteFile( + new_file_path, + new_index.clone(), + )); + } + + operations + } + + pub fn diff(old: &Self, new: &Self, artifact_directory: &Path) -> Vec { + let mut operations: Vec = Vec::new(); + + let mut new_server_object_entity_name_set = HashSet::new(); + let mut new_selectable_set = HashSet::new(); + + for (new_server_object_entity_name, new_selectable_map) in &new.nested_files { + new_server_object_entity_name_set.insert(*new_server_object_entity_name); + let new_server_object_path = artifact_directory.join(*new_server_object_entity_name); + + let old_selectables_for_object = old.nested_files.get(new_server_object_entity_name); + + for (new_selectable, new_files) in new_selectable_map { + new_selectable_set.insert((*new_server_object_entity_name, *new_selectable)); + let new_selectable_path = new_server_object_path.join(new_selectable); + let old_files_for_selectable = + old_selectables_for_object.and_then(|s| s.get(new_selectable)); + + if old_files_for_selectable.is_none() { + operations.push(FileSystemOperation::CreateDirectory( + new_selectable_path.clone(), + )); + } + + for (new_file_name, (new_index, new_hash)) in new_files { + let new_file_path = new_selectable_path.join(new_file_name); + + let old_file = old_files_for_selectable.and_then(|f| f.get(new_file_name)); + + let should_write = old_file + .map(|(_, old_hash)| old_hash != new_hash) + .unwrap_or(true); + + if should_write { + operations.push(FileSystemOperation::WriteFile( + new_file_path, + new_index.clone(), + )); + } + } + } + } + + for (new_file_name, (new_index, new_hash)) in &new.root_files { + let new_file_path = artifact_directory.join(new_file_name); + + let should_write = old + .root_files + .get(new_file_name) + .map(|(_, old_hash)| old_hash != new_hash) + .unwrap_or(true); + + if should_write { + operations.push(FileSystemOperation::WriteFile( + new_file_path, + new_index.clone(), + )); + } + } + + for (old_server_object_entity_name, old_selectable_map) in &old.nested_files { + let old_server_object_path = artifact_directory.join(*old_server_object_entity_name); + + if !new_server_object_entity_name_set.contains(old_server_object_entity_name) { + operations.push(FileSystemOperation::DeleteDirectory(old_server_object_path)); + continue; + } + + let new_selectable_map_for_object = new.nested_files.get(old_server_object_entity_name); + + for (old_selectable, old_files) in old_selectable_map { + let old_selectable_path = old_server_object_path.join(old_selectable); + + if !new_selectable_set.contains(&(*old_server_object_entity_name, *old_selectable)) + { + operations.push(FileSystemOperation::DeleteDirectory(old_selectable_path)); + continue; + } + + let new_files_for_selectable = + new_selectable_map_for_object.and_then(|s| s.get(old_selectable)); + + for file_name in old_files.keys() { + let new_file = new_files_for_selectable.and_then(|f| f.get(file_name)); + + if new_file.is_none() { + let file_path = old_selectable_path.join(file_name); + operations.push(FileSystemOperation::DeleteFile(file_path)); + } + } + } + } + + for file_name in old.root_files.keys() { + if !new.root_files.contains_key(file_name) { + let file_path = artifact_directory.join(file_name); + operations.push(FileSystemOperation::DeleteFile(file_path)); + } + } + + operations + } +} + +#[expect(clippy::type_complexity)] +impl From<&[ArtifactPathAndContent]> for FileSystemState { + fn from(artifacts: &[ArtifactPathAndContent]) -> Self { + let mut root_files = HashMap::new(); + let mut nested_files: HashMap< + ServerObjectEntityName, + HashMap, ArtifactHash)>>, + > = HashMap::new(); + + for (index, artifact) in artifacts.iter().enumerate() { + let value = ( + Index::new(index), + ArtifactHash::from(hash( + &artifact.file_content, + PersistedDocumentsHashAlgorithm::Md5, + )), + ); + + match &artifact.artifact_path.type_and_field { + Some(type_and_field) => { + nested_files + .entry(type_and_field.parent_object_entity_name) + .or_default() + .entry(type_and_field.selectable_name) + .or_default() + .insert(artifact.artifact_path.file_name, value); + } + None => { + root_files.insert(artifact.artifact_path.file_name, value); + } + } + } + + FileSystemState { + root_files, + nested_files, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use common_lang_types::{ + ArtifactPath, ParentObjectEntityNameAndSelectableName, SelectableName, + ServerObjectEntityName, + }; + use intern::string_key::Intern; + use std::path::PathBuf; + + fn create_artifact( + server: Option<&str>, + selectable: Option<&str>, + file_name: &str, + content: &str, + ) -> ArtifactPathAndContent { + let type_and_field = match (server, selectable) { + (Some(s), Some(sel)) => Some(ParentObjectEntityNameAndSelectableName { + parent_object_entity_name: ServerObjectEntityName::from(s.intern()), + selectable_name: SelectableName::from(sel.intern()), + }), + _ => None, + }; + + ArtifactPathAndContent { + artifact_path: ArtifactPath { + type_and_field, + file_name: file_name.intern().into(), + }, + file_content: FileContent::from(content.to_string()), + } + } + + #[test] + fn test_insert_root_file() { + let artifact = create_artifact(None, None, "package.json", "{}"); + let state = FileSystemState::from(&[artifact][..]); + + assert_eq!(state.root_files.len(), 1); + assert!( + state + .root_files + .contains_key(&ArtifactFileName::from("package.json".intern())) + ); + } + + #[test] + fn test_insert_nested_file() { + let artifact = create_artifact( + Some("User"), + Some("name"), + "query.graphql", + "query { user { name } }", + ); + + let state = FileSystemState::from(&[artifact][..]); + + assert_eq!(state.nested_files.len(), 1); + + let server = &ServerObjectEntityName::from("User".intern()); + let selectable = SelectableName::from("name".intern()); + let file_name = &ArtifactFileName::from("query.graphql".intern()); + + assert!( + state + .nested_files + .get(server) + .and_then(|s| s.get(&selectable)) + .and_then(|f| f.get(file_name)) + .is_some() + ); + } + + #[test] + fn test_from_artifacts() { + let artifacts = vec![ + create_artifact(None, None, "schema.graphql", "type Query"), + create_artifact(Some("User"), Some("name"), "query.graphql", "query {}"), + create_artifact( + Some("User"), + Some("email"), + "mutation.graphql", + "mutation {}", + ), + ]; + + let state = FileSystemState::from(&artifacts[..]); + + assert_eq!(state.root_files.len(), 1); + assert_eq!(state.nested_files.len(), 1); + + let user_server = &ServerObjectEntityName::from("User".intern()); + let selectables = state.nested_files.get(user_server).unwrap(); + assert_eq!(selectables.len(), 2); + } + + #[test] + fn test_diff_empty_to_new() { + let artifacts = vec![create_artifact( + Some("User"), + Some("name"), + "query.graphql", + "query", + )]; + let new_state = FileSystemState::from(&artifacts[..]); + + let artifact_dir = PathBuf::from("/__isograph"); + let ops = FileSystemState::recreate_all(&new_state, &artifact_dir); + + assert!(matches!(ops[0], FileSystemOperation::DeleteDirectory(_))); + + let create_dirs = ops + .iter() + .filter(|op| matches!(op, FileSystemOperation::CreateDirectory(_))) + .count(); + let write_files = ops + .iter() + .filter(|op| matches!(op, FileSystemOperation::WriteFile(_, _))) + .count(); + + assert_eq!(create_dirs, 1); + assert_eq!(write_files, 1); + } + + #[test] + fn test_diff_no_changes() { + let artifact1 = vec![create_artifact(None, None, "file.txt", "content")]; + let artifact2 = vec![create_artifact(None, None, "file.txt", "content")]; + + let old_state = FileSystemState::from(&artifact1[..]); + let new_state = FileSystemState::from(&artifact2[..]); + + let artifact_dir = PathBuf::from("/__isograph"); + let ops = FileSystemState::diff(&old_state, &new_state, &artifact_dir); + assert_eq!(ops.len(), 0); + } + + #[test] + fn test_diff_file_content_() { + let old_artifacts = vec![create_artifact(None, None, "file.txt", "old content")]; + let new_artifacts = vec![create_artifact(None, None, "file.txt", "new content")]; + + let old_state = FileSystemState::from(&old_artifacts[..]); + let new_state = FileSystemState::from(&new_artifacts[..]); + + let artifact_dir = PathBuf::from("/__isograph"); + let ops = FileSystemState::diff(&old_state, &new_state, &artifact_dir); + + assert_eq!(ops.len(), 1); + assert!(matches!(ops[0], FileSystemOperation::WriteFile(_, _))); + } + + #[test] + fn test_diff_add_new_file() { + let old_artifacts = vec![create_artifact(None, None, "existing.txt", "content")]; + let new_artifacts = vec![ + create_artifact(None, None, "existing.txt", "content"), + create_artifact(None, None, "new.txt", "new content"), + ]; + + let old_state = FileSystemState::from(&old_artifacts[..]); + let new_state = FileSystemState::from(&new_artifacts[..]); + + let artifact_dir = PathBuf::from("/__isograph"); + let ops = FileSystemState::diff(&old_state, &new_state, &artifact_dir); + + assert_eq!(ops.len(), 1); + if let FileSystemOperation::WriteFile(path, _) = &ops[0] { + assert!(path.ends_with("new.txt")); + } else { + panic!("Expected WriteFile operation"); + } + } + + #[test] + fn test_diff_delete_file() { + let old_artifacts = vec![ + create_artifact(None, None, "keep.txt", "content"), + create_artifact(None, None, "delete.txt", "content"), + ]; + let new_artifacts = vec![create_artifact(None, None, "keep.txt", "content")]; + + let old_state = FileSystemState::from(&old_artifacts[..]); + let new_state = FileSystemState::from(&new_artifacts[..]); + + let artifact_dir = PathBuf::from("/__isograph"); + let ops = FileSystemState::diff(&old_state, &new_state, &artifact_dir); + + assert_eq!(ops.len(), 1); + if let FileSystemOperation::DeleteFile(path) = &ops[0] { + assert!(path.ends_with("delete.txt")); + } else { + panic!("Expected DeleteFile operation"); + } + } + + #[test] + fn test_diff_delete_empty_directory() { + let old_artifacts = vec![create_artifact( + Some("User"), + Some("name"), + "query.graphql", + "query", + )]; + let old_state = FileSystemState::from(&old_artifacts[..]); + + let new_state = FileSystemState::default(); + + let artifact_dir = PathBuf::from("/__isograph"); + let ops = FileSystemState::diff(&old_state, &new_state, &artifact_dir); + + assert_eq!(ops.len(), 1); + assert!(matches!(ops[0], FileSystemOperation::DeleteDirectory(_))); + } + + #[test] + fn test_diff_nested_file_changes() { + let old_artifacts = vec![create_artifact( + Some("User"), + Some("name"), + "query.graphql", + "old query", + )]; + let new_artifacts = vec![create_artifact( + Some("User"), + Some("name"), + "query.graphql", + "new query", + )]; + + let old_state = FileSystemState::from(&old_artifacts[..]); + let new_state = FileSystemState::from(&new_artifacts[..]); + + let artifact_dir = PathBuf::from("/__isograph"); + let ops = FileSystemState::diff(&old_state, &new_state, &artifact_dir); + + assert_eq!(ops.len(), 1); + assert!(matches!(ops[0], FileSystemOperation::WriteFile(_, _))); + } + + #[test] + fn test_diff_add_new_selectable() { + let old_artifacts = vec![create_artifact( + Some("User"), + Some("name"), + "query.graphql", + "query", + )]; + let new_artifacts = vec![ + create_artifact(Some("User"), Some("name"), "query.graphql", "query"), + create_artifact(Some("User"), Some("email"), "query.graphql", "query"), + ]; + + let old_state = FileSystemState::from(&old_artifacts[..]); + let new_state = FileSystemState::from(&new_artifacts[..]); + + let artifact_dir = PathBuf::from("/__isograph"); + let ops = FileSystemState::diff(&old_state, &new_state, &artifact_dir); + + assert_eq!(ops.len(), 2); + assert!(matches!(ops[0], FileSystemOperation::CreateDirectory(_))); + assert!(matches!(ops[1], FileSystemOperation::WriteFile(_, _))); + } + + #[test] + fn test_diff_complex_scenario() { + let old_artifacts = vec![ + create_artifact(None, None, "root.txt", "old root"), + create_artifact(Some("User"), Some("name"), "query.graphql", "old query"), + create_artifact(Some("User"), Some("email"), "query.graphql", "delete me"), + create_artifact(Some("Post"), Some("title"), "query.graphql", "post query"), + ]; + let new_artifacts = vec![ + create_artifact(None, None, "root.txt", "new root"), + create_artifact(None, None, "new_root.txt", "new file"), + create_artifact(Some("User"), Some("name"), "query.graphql", "new query"), + create_artifact(Some("Post"), Some("title"), "query.graphql", "post query"), + create_artifact(Some("Comment"), Some("text"), "query.graphql", "comment"), + ]; + + let old_state = FileSystemState::from(&old_artifacts[..]); + let new_state = FileSystemState::from(&new_artifacts[..]); + + let artifact_dir = PathBuf::from("/__isograph"); + let ops = FileSystemState::diff(&old_state, &new_state, &artifact_dir); + + let writes = ops + .iter() + .filter(|op| matches!(op, FileSystemOperation::WriteFile(_, _))) + .count(); + let deletes = ops + .iter() + .filter(|op| matches!(op, FileSystemOperation::DeleteFile(_))) + .count(); + let create_dirs = ops + .iter() + .filter(|op| matches!(op, FileSystemOperation::CreateDirectory(_))) + .count(); + let delete_dirs = ops + .iter() + .filter(|op| matches!(op, FileSystemOperation::DeleteDirectory(_))) + .count(); + + assert!(writes >= 3); // root.txt, new_root.txt, User/name/query.graphql, Comment/text/query.graphql + assert_eq!(deletes, 0); + assert!(create_dirs >= 1); // Comment/text + assert_eq!(delete_dirs, 1); // User/email directory + } +} diff --git a/crates/artifact_content/src/lib.rs b/crates/artifact_content/src/lib.rs index b9ab8837d..9c2bc7bf2 100644 --- a/crates/artifact_content/src/lib.rs +++ b/crates/artifact_content/src/lib.rs @@ -1,16 +1,18 @@ mod eager_reader_artifact; mod entrypoint_artifact; +mod file_system_state; mod format_parameter_type; pub mod generate_artifacts; mod imperatively_loaded_fields; mod import_statements; mod iso_overload_file; mod normalization_ast_text; -mod operation_text; +pub mod operation_text; mod persisted_documents; mod raw_response_type; mod reader_ast; mod refetch_reader_artifact; mod ts_config; +pub use file_system_state::FileSystemState; pub use generate_artifacts::get_artifact_path_and_content; diff --git a/crates/artifact_content/src/operation_text.rs b/crates/artifact_content/src/operation_text.rs index 238703187..eb501a7c9 100644 --- a/crates/artifact_content/src/operation_text.rs +++ b/crates/artifact_content/src/operation_text.rs @@ -68,7 +68,7 @@ pub(crate) fn generate_operation_text<'a, TNetworkProtocol: NetworkProtocol>( } } -fn hash(data: &str, algorithm: PersistedDocumentsHashAlgorithm) -> String { +pub fn hash(data: &str, algorithm: PersistedDocumentsHashAlgorithm) -> String { match algorithm { PersistedDocumentsHashAlgorithm::Md5 => { let mut md5 = Md5::new(); diff --git a/crates/common_lang_types/src/file_system_operation.rs b/crates/common_lang_types/src/file_system_operation.rs new file mode 100644 index 000000000..1ec010565 --- /dev/null +++ b/crates/common_lang_types/src/file_system_operation.rs @@ -0,0 +1,13 @@ +use std::path::PathBuf; + +use crate::FileContent; + +use pico::Index; + +#[derive(Debug, Clone)] +pub enum FileSystemOperation { + DeleteDirectory(PathBuf), + CreateDirectory(PathBuf), + WriteFile(PathBuf, Index), + DeleteFile(PathBuf), +} diff --git a/crates/common_lang_types/src/lib.rs b/crates/common_lang_types/src/lib.rs index 02e801148..789a4d8da 100644 --- a/crates/common_lang_types/src/lib.rs +++ b/crates/common_lang_types/src/lib.rs @@ -1,6 +1,7 @@ mod absolute_and_relative_path; mod diagnostic; mod entity_and_selectable_name; +mod file_system_operation; mod location; mod path_and_content; mod selectable_name; @@ -12,6 +13,7 @@ mod text_with_carats; pub use absolute_and_relative_path::*; pub use diagnostic::*; pub use entity_and_selectable_name::*; +pub use file_system_operation::*; pub use location::*; pub use path_and_content::*; pub use selectable_name::*; diff --git a/crates/common_lang_types/src/path_and_content.rs b/crates/common_lang_types/src/path_and_content.rs index 1d7cb813d..26d41edd6 100644 --- a/crates/common_lang_types/src/path_and_content.rs +++ b/crates/common_lang_types/src/path_and_content.rs @@ -1,5 +1,6 @@ use crate::{ArtifactFileName, ParentObjectEntityNameAndSelectableName}; +#[derive(Debug, Clone)] pub struct FileContent(pub String); impl From for FileContent { @@ -31,3 +32,32 @@ pub struct ArtifactPath { pub type_and_field: Option, pub file_name: ArtifactFileName, } + +#[derive(Debug, Clone)] +pub struct ArtifactHash(String); + +impl From for ArtifactHash { + fn from(value: String) -> Self { + ArtifactHash(value) + } +} + +impl std::fmt::Display for ArtifactHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::ops::Deref for ArtifactHash { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PartialEq for ArtifactHash { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} diff --git a/crates/isograph_compiler/src/batch_compile.rs b/crates/isograph_compiler/src/batch_compile.rs index cc593e869..db244cdf2 100644 --- a/crates/isograph_compiler/src/batch_compile.rs +++ b/crates/isograph_compiler/src/batch_compile.rs @@ -1,13 +1,14 @@ use std::{path::PathBuf, time::Duration}; use crate::{ - compiler_state::CompilerState, with_duration::WithDuration, - write_artifacts::write_artifacts_to_disk, + compiler_state::CompilerState, + with_duration::WithDuration, + write_artifacts::{apply_file_system_operations, get_file_system_operations}, }; use artifact_content::get_artifact_path_and_content; use colored::Colorize; use common_lang_types::{CurrentWorkingDirectory, DiagnosticVecResult}; -use isograph_schema::{IsographDatabase, NetworkProtocol}; +use isograph_schema::NetworkProtocol; use prelude::Postfix; use pretty_duration::pretty_duration; use tracing::{error, info}; @@ -24,8 +25,10 @@ pub fn compile_and_print( current_working_directory: CurrentWorkingDirectory, ) -> DiagnosticVecResult<()> { info!("{}", "Starting to compile.".cyan()); - let state = CompilerState::new(config_location, current_working_directory)?; - print_result(WithDuration::new(|| compile::(&state.db))) + let mut state = CompilerState::new(config_location, current_working_directory)?; + print_result(WithDuration::new(|| { + compile::(&mut state) + })) } pub fn print_result( @@ -79,16 +82,23 @@ fn print_stats(elapsed_time: Duration, stats: CompilationStats) { /// This the "workhorse" command of batch compilation. #[tracing::instrument(skip_all)] pub fn compile( - db: &IsographDatabase, + state: &mut CompilerState, ) -> DiagnosticVecResult { // Note: we calculate all of the artifact paths and contents first, so that writing to // disk can be as fast as possible and we minimize the chance that changes to the file // system occur while we're writing and we get unpredictable results. - + let db = &state.db; let config = db.get_isograph_config(); let (artifacts, stats) = get_artifact_path_and_content(db)?; + + let file_system_operations = get_file_system_operations( + &artifacts, + &config.artifact_directory.absolute_path, + &mut state.file_system_state, + ); + let total_artifacts_written = - write_artifacts_to_disk(artifacts, &config.artifact_directory.absolute_path)?; + apply_file_system_operations(&file_system_operations, &artifacts)?; CompilationStats { client_field_count: stats.client_field_count, diff --git a/crates/isograph_compiler/src/compiler_state.rs b/crates/isograph_compiler/src/compiler_state.rs index 5262ac77a..5e6995b39 100644 --- a/crates/isograph_compiler/src/compiler_state.rs +++ b/crates/isograph_compiler/src/compiler_state.rs @@ -10,6 +10,7 @@ use pico::Database; use prelude::Postfix; use crate::source_files::initialize_sources; +use artifact_content::FileSystemState; const GC_DURATION_SECONDS: u64 = 60; @@ -17,6 +18,7 @@ const GC_DURATION_SECONDS: u64 = 60; pub struct CompilerState { pub db: IsographDatabase, pub last_gc_run: Instant, + pub file_system_state: Option, } impl CompilerState { @@ -31,6 +33,7 @@ impl CompilerState { Self { db, last_gc_run: Instant::now(), + file_system_state: None, } .wrap_ok() } diff --git a/crates/isograph_compiler/src/watch.rs b/crates/isograph_compiler/src/watch.rs index e675efd85..088396db8 100644 --- a/crates/isograph_compiler/src/watch.rs +++ b/crates/isograph_compiler/src/watch.rs @@ -30,7 +30,9 @@ pub async fn handle_watch_command( let config = state.db.get_isograph_config().clone(); info!("{}", "Starting to compile.".cyan()); - let _ = print_result(WithDuration::new(|| compile::(&state.db))); + let _ = print_result(WithDuration::new(|| { + compile::(&mut state) + })); let (mut file_system_receiver, mut file_system_watcher) = create_debounced_file_watcher(&config); @@ -51,7 +53,7 @@ pub async fn handle_watch_command( info!("{}", "File changes detected. Starting to compile.".cyan()); update_sources(&mut state.db, &changes)?; }; - let result = WithDuration::new(|| compile::(&state.db)); + let result = WithDuration::new(|| compile::(&mut state)); let _ = print_result(result); state.run_garbage_collection(); } diff --git a/crates/isograph_compiler/src/write_artifacts.rs b/crates/isograph_compiler/src/write_artifacts.rs index c8851e343..07fe7f88a 100644 --- a/crates/isograph_compiler/src/write_artifacts.rs +++ b/crates/isograph_compiler/src/write_artifacts.rs @@ -1,69 +1,84 @@ use std::{ - fs::{self, File}, - io::Write, - path::PathBuf, + fs, + path::{Path, PathBuf}, }; -use common_lang_types::{ArtifactPathAndContent, Diagnostic, DiagnosticResult}; -use intern::string_key::Lookup; +use common_lang_types::{ + ArtifactPathAndContent, Diagnostic, DiagnosticResult, FileSystemOperation, +}; + +use artifact_content::FileSystemState; #[tracing::instrument(skip_all)] -pub(crate) fn write_artifacts_to_disk( - paths_and_contents: impl IntoIterator, - artifact_directory: &PathBuf, -) -> DiagnosticResult { - if artifact_directory.exists() { - fs::remove_dir_all(artifact_directory).map_err(|e| { - unable_to_do_something_at_path_diagnostic( - artifact_directory, - &e.to_string(), - "delete directory", - ) - })?; - } - fs::create_dir_all(artifact_directory).map_err(|e| { - let message = e.to_string(); - unable_to_do_something_at_path_diagnostic(artifact_directory, &message, "create directory") - })?; +pub(crate) fn get_file_system_operations( + paths_and_contents: &[ArtifactPathAndContent], + artifact_directory: &Path, + file_system_state: &mut Option, +) -> Vec { + let new_file_system_state = paths_and_contents.into(); + let operations = match file_system_state { + None => FileSystemState::recreate_all(&new_file_system_state, artifact_directory), + Some(file_system_state) => FileSystemState::diff( + file_system_state, + &new_file_system_state, + artifact_directory, + ), + }; + *file_system_state = Some(new_file_system_state); + operations +} +#[tracing::instrument(skip_all)] +pub(crate) fn apply_file_system_operations( + operations: &[FileSystemOperation], + artifacts: &[ArtifactPathAndContent], +) -> DiagnosticResult { let mut count = 0; - for path_and_content in paths_and_contents { - // Is this better than materializing paths_and_contents sooner? - count += 1; - - let absolute_directory = match path_and_content.artifact_path.type_and_field { - Some(type_and_field) => artifact_directory - .join(type_and_field.parent_object_entity_name.lookup()) - .join(type_and_field.selectable_name.lookup()), - None => artifact_directory.clone(), - }; - fs::create_dir_all(&absolute_directory).map_err(|e| { - unable_to_do_something_at_path_diagnostic( - &absolute_directory, - &e.to_string(), - "create directory", - ) - })?; - let absolute_file_path = - absolute_directory.join(path_and_content.artifact_path.file_name.lookup()); - let mut file = File::create(&absolute_file_path).map_err(|e| { - unable_to_do_something_at_path_diagnostic( - &absolute_file_path, - &e.to_string(), - "create file", - ) - })?; - - file.write(path_and_content.file_content.as_bytes()) - .map_err(|e| { - unable_to_do_something_at_path_diagnostic( - &absolute_file_path, - &e.to_string(), - "write contents of file", - ) - })?; + for operation in operations { + match operation { + FileSystemOperation::DeleteDirectory(path) => { + if path.exists() { + fs::remove_dir_all(path.clone()).map_err(|e| { + unable_to_do_something_at_path_diagnostic( + path, + &e.to_string(), + "delete directory", + ) + })?; + } + } + FileSystemOperation::CreateDirectory(path) => { + count += 1; + fs::create_dir_all(path.clone()).map_err(|e| { + unable_to_do_something_at_path_diagnostic( + path, + &e.to_string(), + "create directory", + ) + })?; + } + FileSystemOperation::WriteFile(path, content) => { + let content = &artifacts + .get(content.idx) + .expect("index should be valid for artifacts vec") + .file_content; + fs::write(path.clone(), content.as_bytes()).map_err(|e| { + unable_to_do_something_at_path_diagnostic( + path, + &e.to_string(), + "write contents of file", + ) + })?; + } + FileSystemOperation::DeleteFile(path) => { + fs::remove_file(path.clone()).map_err(|e| { + unable_to_do_something_at_path_diagnostic(path, &e.to_string(), "delete file") + })?; + } + } } + Ok(count) } diff --git a/crates/pico/src/lib.rs b/crates/pico/src/lib.rs index a61dde2e5..fa3da7a52 100644 --- a/crates/pico/src/lib.rs +++ b/crates/pico/src/lib.rs @@ -19,6 +19,7 @@ pub use database::*; pub use derived_node::*; pub use dyn_eq::*; pub use execute_memoized_function::*; +pub use index::*; pub use intern::*; pub use memo_ref::*; pub use raw_ptr::*;