From f38c96e12badff4316fd0074d04c4e7f4f9c81db Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 5 Feb 2026 18:46:43 -0600 Subject: [PATCH] Add init file discovery in JobThreadContext and enhance script renaming logic - Introduced a new method `find_init_file` to locate the first `init.*.luau` or `init.*.lua` file within a directory-format script folder. - Updated the script renaming logic to handle directory-format scripts correctly by renaming the init file instead of the directory itself. - Enhanced logging to provide warnings when no init file is found, improving debugging capabilities during class name changes. - Adjusted related file handling to ensure proper file path management during script transitions. --- src/change_processor.rs | 139 +++++++++++++++++++++++++++------------- src/web/api.rs | 17 ++++- 2 files changed, 111 insertions(+), 45 deletions(-) diff --git a/src/change_processor.rs b/src/change_processor.rs index f6da9206..756423c7 100644 --- a/src/change_processor.rs +++ b/src/change_processor.rs @@ -2,7 +2,7 @@ use crossbeam_channel::{select, Receiver, RecvError, Sender}; use jod_thread::JoinHandle; use memofs::{IoResultExt, Vfs, VfsEvent}; use rbx_dom_weak::types::{Ref, Variant}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; use std::{ fs, @@ -134,6 +134,31 @@ struct JobThreadContext { } impl JobThreadContext { + /// Find the init file inside a directory-format script folder. + /// Returns the path to the first `init.*.luau` or `init.*.lua` found. + fn find_init_file(dir: &Path) -> Option { + // Check known init file names in priority order + let candidates = [ + "init.luau", + "init.server.luau", + "init.client.luau", + "init.local.luau", + "init.plugin.luau", + "init.legacy.luau", + "init.lua", + "init.server.lua", + "init.client.lua", + "init.local.lua", + ]; + for name in &candidates { + let candidate = dir.join(name); + if candidate.exists() { + return Some(candidate); + } + } + None + } + /// Computes and applies patches to the DOM for a given file path. /// /// This function finds the nearest ancestor to the given path that has associated instances @@ -538,52 +563,78 @@ impl JobThreadContext { ); if old_is_script && new_is_script && path.exists() { - // Script-to-script transition: rename the file extension - if let Some(parent) = path.parent() { - let name = instance.name(); - let new_suffix = match new_class.as_str() { - "ModuleScript" => "", - "Script" => ".server", - "LocalScript" => ".local", - _ => "", - }; - let is_init = path - .file_name() - .and_then(|f| f.to_str()) - .map(|f| f.starts_with("init.")) - .unwrap_or(false); - - let new_file_name = if is_init { - if new_suffix.is_empty() { - "init.luau".to_string() - } else { - format!("init{}.luau", new_suffix) - } - } else if new_suffix.is_empty() { - format!("{}.luau", name) - } else { - format!("{}{}.luau", name, new_suffix) - }; - - let new_path = parent.join(&new_file_name); - if new_path != *path { - log::info!( - "Two-way sync: Changing class {} -> {}, \ - renaming {} -> {}", - old_class, - new_class, - path.display(), - new_path.display() - ); - if let Err(err) = fs::rename(path, &new_path) { - log::error!( - "Failed to rename {:?} to {:?}: {}", - path, - new_path, - err + // Script-to-script transition: rename the file extension. + // For directory-format scripts, the path is the directory + // (e.g., src/MyModule/), not the init file inside. We must + // find and rename the init file, not the directory. + let (actual_file, file_parent) = if path.is_dir() { + // Find the init file inside the directory + let init_file = Self::find_init_file(path); + match init_file { + Some(f) => (f.clone(), path.to_path_buf()), + None => { + log::warn!( + "Cannot change ClassName for directory {} \ + — no init file found inside", + path.display() ); + continue; } } + } else { + ( + path.clone(), + path.parent() + .unwrap_or(path.as_path()) + .to_path_buf(), + ) + }; + + let name = instance.name(); + let new_suffix = match new_class.as_str() { + "ModuleScript" => "", + "Script" => ".server", + "LocalScript" => ".local", + _ => "", + }; + let is_init = actual_file + .file_name() + .and_then(|f| f.to_str()) + .map(|f| f.starts_with("init.")) + .unwrap_or(false); + + let new_file_name = if is_init { + if new_suffix.is_empty() { + "init.luau".to_string() + } else { + format!("init{}.luau", new_suffix) + } + } else if new_suffix.is_empty() { + format!("{}.luau", name) + } else { + format!("{}{}.luau", name, new_suffix) + }; + + let new_path = file_parent.join(&new_file_name); + if new_path != actual_file { + log::info!( + "Two-way sync: Changing class {} -> {}, \ + renaming {} -> {}", + old_class, + new_class, + actual_file.display(), + new_path.display() + ); + if let Err(err) = + fs::rename(&actual_file, &new_path) + { + log::error!( + "Failed to rename {:?} to {:?}: {}", + actual_file, + new_path, + err + ); + } } } else if old_is_script != new_is_script { log::warn!( diff --git a/src/web/api.rs b/src/web/api.rs index 2802bd41..cb60e36a 100644 --- a/src/web/api.rs +++ b/src/web/api.rs @@ -2057,7 +2057,22 @@ impl ApiService { .as_ref() .context("Instance has no filesystem path")?; - let inst_path = instigating_source.path(); + // ProjectNode instances are defined in the project file. Writing meta + // JSON to the project file path would corrupt it. Only filesystem-backed + // instances (InstigatingSource::Path) can have properties persisted. + let inst_path = match instigating_source { + InstigatingSource::Path(p) => p.as_path(), + InstigatingSource::ProjectNode { name, .. } => { + log::warn!( + "Cannot persist non-Source properties for instance '{}' (id: {:?}) — \ + it is defined in a project file. Edit the project file directly.", + name, + update.id + ); + return Ok(()); + } + }; + let class_name = instance.class_name(); let is_script = matches!( class_name.as_str(),