This commit is contained in:
Mohammad Abo-Al-Nadar 2026-10-06 01:20:58 -04:00 • committed by GitHub
commit a3c65eec09
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 219 additions and 17 deletions

View file

@ -1,9 +1,11 @@
name: CI name: CI
on: on:
workflow_dispatch:
push: push:
branches: branches:
- master - master
- "feat/**"
pull_request: pull_request:
branches: branches:

View file

@ -25,6 +25,10 @@ impl Glob {
pub fn is_match<P: AsRef<Path>>(&self, path: P) -> bool { pub fn is_match<P: AsRef<Path>>(&self, path: P) -> bool {
self.matcher.is_match(path) self.matcher.is_match(path)
} }
pub fn as_str(&self) -> &str {
self.inner.glob()
}
} }
impl PartialEq for Glob { impl PartialEq for Glob {

View file

@ -205,6 +205,20 @@ impl InstanceContext {
pub fn get_user_sync_rule(&self, path: &Path) -> Option<&SyncRule> { pub fn get_user_sync_rule(&self, path: &Path) -> Option<&SyncRule> {
self.sync_rules.iter().find(|&rule| rule.matches(path)) self.sync_rules.iter().find(|&rule| rule.matches(path))
} }
/// Returns the first sync rule whose child_pattern matches the given file name.
pub fn get_user_sync_rule_for_child(&self, file_name: &str) -> Option<&SyncRule> {
self.sync_rules
.iter()
.find(|rule| rule.matches_child(file_name))
}
/// Returns whether any user sync rule has a child_pattern matching this file name.
pub fn is_child_pattern_match(&self, file_name: &str) -> bool {
self.sync_rules
.iter()
.any(|rule| rule.matches_child(file_name))
}
} }
impl Default for InstanceContext { impl Default for InstanceContext {
@ -329,6 +343,14 @@ pub struct SyncRule {
/// If not specified, the file extension is the only thing cut off. /// If not specified, the file extension is the only thing cut off.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub suffix: Option<String>, pub suffix: Option<String>,
/// A pattern matching child init files within a directory for this SyncRule.
#[serde(
rename = "child_pattern",
alias = "childPattern",
default,
skip_serializing_if = "Option::is_none"
)]
pub child_pattern: Option<Glob>,
/// The 'base' of the glob above, allowing it to be used /// The 'base' of the glob above, allowing it to be used
/// relative to a path instead of absolute. /// relative to a path instead of absolute.
#[serde(skip)] #[serde(skip)]
@ -351,6 +373,38 @@ impl SyncRule {
} }
} }
/// Returns whether this rule matches the given child file name.
pub fn matches_child(&self, file_name: &str) -> bool {
if let Some(pattern) = &self.child_pattern {
return pattern.is_match(file_name);
}
if let Some(suffix) = &self.suffix {
let expected = format!("init{suffix}");
return file_name == expected;
}
false
}
/// Returns whether this rule matches the given child path.
pub fn matches_child_path(&self, path: &Path) -> bool {
if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
if !self.matches_child(file_name) {
return false;
}
if !self.base_path.as_os_str().is_empty() {
if let Ok(suffix) = path.strip_prefix(&self.base_path) {
if let Some(exclude) = &self.exclude {
if exclude.is_match(suffix) {
return false;
}
}
}
}
return true;
}
false
}
pub fn file_name_for_path<'a>(&self, path: &'a Path) -> anyhow::Result<&'a str> { pub fn file_name_for_path<'a>(&self, path: &'a Path) -> anyhow::Result<&'a str> {
if let Some(suffix) = &self.suffix { if let Some(suffix) = &self.suffix {
let file_name = path let file_name = path

View file

@ -61,7 +61,7 @@ pub fn snapshot_dir_no_meta(
} }
let normalized_path = vfs.canonicalize(path)?; let normalized_path = vfs.canonicalize(path)?;
let relevant_paths = vec![ let mut relevant_paths = vec![
normalized_path.clone(), normalized_path.clone(),
// TODO: We shouldn't need to know about Lua existing in this // TODO: We shouldn't need to know about Lua existing in this
// middleware. Should we figure out a way for that function to add // middleware. Should we figure out a way for that function to add
@ -77,6 +77,19 @@ pub fn snapshot_dir_no_meta(
normalized_path.join("init.csv"), normalized_path.join("init.csv"),
]; ];
for rule in &context.sync_rules {
if let Some(child_pattern) = &rule.child_pattern {
let pattern_str = child_pattern.as_str();
if !pattern_str.contains('*')
&& !pattern_str.contains('?')
&& !pattern_str.contains('[')
&& !pattern_str.contains('{')
{
relevant_paths.push(normalized_path.join(pattern_str));
}
}
}
let snapshot = InstanceSnapshot::new() let snapshot = InstanceSnapshot::new()
.name(name) .name(name)
.class_name("Folder") .class_name("Folder")

View file

@ -73,14 +73,10 @@ pub fn snapshot_from_vfs(
}; };
if meta.is_dir() { if meta.is_dir() {
let (middleware, dir_name, init_path) = get_dir_middleware(vfs, path)?; let (middleware, dir_name, init_path) = get_dir_middleware(vfs, path, &context.sync_rules)?;
// TODO: Support user defined init paths
// If and when we do, make sure to go support it in
// `Project::set_file_name`, as right now it special-cases
// `default.project.json` as an `init` path.
match middleware { match middleware {
Middleware::Dir => middleware.snapshot(context, vfs, path, dir_name), Middleware::Dir => middleware.snapshot(context, vfs, path, dir_name),
_ => middleware.snapshot(context, vfs, &init_path, dir_name), _ => middleware.snapshot_init(context, vfs, &init_path, dir_name),
} }
} else { } else {
let file_name = path let file_name = path
@ -97,19 +93,24 @@ pub fn snapshot_from_vfs(
_ => {} _ => {}
} }
if context.is_child_pattern_match(file_name) {
return Ok(None);
}
snapshot_from_path(context, vfs, path) snapshot_from_path(context, vfs, path)
} }
} }
/// Gets the appropriate middleware for a directory by checking for `init` /// Gets the appropriate middleware for a directory by checking for `init`
/// files. This uses an intrinsic priority list and for compatibility, /// files. This first checks user-defined sync rules with `child_pattern` or `suffix`,
/// that order should be left unchanged. /// then falls back to an intrinsic priority list.
/// ///
/// Returns the middleware, the name of the directory, and the path to /// Returns the middleware, the name of the directory, and the path to
/// the init location. /// the init location.
fn get_dir_middleware<'path>( fn get_dir_middleware<'path>(
vfs: &Vfs, vfs: &Vfs,
dir_path: &'path Path, dir_path: &'path Path,
sync_rules: &[SyncRule],
) -> anyhow::Result<(Middleware, &'path str, PathBuf)> { ) -> anyhow::Result<(Middleware, &'path str, PathBuf)> {
let dir_name = dir_path let dir_name = dir_path
.file_name() .file_name()
@ -117,6 +118,31 @@ fn get_dir_middleware<'path>(
.to_str() .to_str()
.ok_or_else(|| anyhow::anyhow!("File name was not valid UTF-8: {}", dir_path.display()))?; .ok_or_else(|| anyhow::anyhow!("File name was not valid UTF-8: {}", dir_path.display()))?;
for default_project_name in DEFAULT_PROJECT_NAMES {
let project_path = dir_path.join(default_project_name);
if vfs.metadata(&project_path).with_not_found()?.is_some() {
return Ok((Middleware::Project, dir_name, project_path));
}
}
if let Ok(entries) = vfs.read_dir(dir_path) {
let mut child_entries = Vec::new();
for entry in entries {
if let Ok(entry) = entry {
let entry_path = entry.path().to_path_buf();
child_entries.push(entry_path);
}
}
for rule in sync_rules {
for entry_path in &child_entries {
if rule.matches_child_path(entry_path) {
return Ok((rule.middleware, dir_name, entry_path.clone()));
}
}
}
}
static INIT_PATHS: OnceLock<Vec<(Middleware, &str)>> = OnceLock::new(); static INIT_PATHS: OnceLock<Vec<(Middleware, &str)>> = OnceLock::new();
let order = INIT_PATHS.get_or_init(|| { let order = INIT_PATHS.get_or_init(|| {
vec![ vec![
@ -132,13 +158,6 @@ fn get_dir_middleware<'path>(
] ]
}); });
for default_project_name in DEFAULT_PROJECT_NAMES {
let project_path = dir_path.join(default_project_name);
if vfs.metadata(&project_path).with_not_found()?.is_some() {
return Ok((Middleware::Project, dir_name, project_path));
}
}
for (middleware, name) in order { for (middleware, name) in order {
let test_path = dir_path.join(name); let test_path = dir_path.join(name);
if vfs.metadata(&test_path).with_not_found()?.is_some() { if vfs.metadata(&test_path).with_not_found()?.is_some() {
@ -349,6 +368,50 @@ impl Middleware {
) )
} }
/// Creates a snapshot for an init file inside a directory, usurping the
/// directory and attaching the directory's other children.
pub fn snapshot_init(
&self,
context: &InstanceContext,
vfs: &Vfs,
init_path: &Path,
name: &str,
) -> anyhow::Result<Option<InstanceSnapshot>> {
let mut output = match self {
Self::ServerScript | Self::ServerScriptDir => {
snapshot_lua_init(context, vfs, init_path, name, ScriptType::Server)
}
Self::ClientScript | Self::ClientScriptDir => {
snapshot_lua_init(context, vfs, init_path, name, ScriptType::Client)
}
Self::PluginScript | Self::PluginScriptDir => {
snapshot_lua_init(context, vfs, init_path, name, ScriptType::Plugin)
}
Self::ModuleScript | Self::ModuleScriptDir => {
snapshot_lua_init(context, vfs, init_path, name, ScriptType::Module)
}
Self::LegacyClientScript => {
snapshot_lua_init(context, vfs, init_path, name, ScriptType::LegacyClient)
}
Self::LegacyServerScript => {
snapshot_lua_init(context, vfs, init_path, name, ScriptType::LegacyServer)
}
Self::RunContextClientScript => {
snapshot_lua_init(context, vfs, init_path, name, ScriptType::RunContextClient)
}
Self::RunContextServerScript => {
snapshot_lua_init(context, vfs, init_path, name, ScriptType::RunContextServer)
}
Self::Csv | Self::CsvDir => snapshot_csv_init(context, vfs, init_path, name),
Self::Project => snapshot_project(context, vfs, init_path, name),
_ => self.snapshot(context, vfs, init_path, name),
};
if let Ok(Some(ref mut snapshot)) = output {
snapshot.metadata.middleware = Some(*self);
}
output
}
/// Attempts to return a middleware that should be used for the given path. /// Attempts to return a middleware that should be used for the given path.
/// ///
/// Returns `Err` only if the Vfs cannot read information about the path. /// Returns `Err` only if the Vfs cannot read information about the path.
@ -363,7 +426,7 @@ impl Middleware {
}; };
if meta.is_dir() { if meta.is_dir() {
let (middleware, _, _) = get_dir_middleware(vfs, path)?; let (middleware, _, _) = get_dir_middleware(vfs, path, sync_rules)?;
Ok(Some(middleware)) Ok(Some(middleware))
} else { } else {
for rule in sync_rules.iter().chain(default_sync_rules()) { for rule in sync_rules.iter().chain(default_sync_rules()) {
@ -390,6 +453,7 @@ macro_rules! sync_rule {
include: Glob::new($pattern).unwrap(), include: Glob::new($pattern).unwrap(),
exclude: None, exclude: None,
suffix: None, suffix: None,
child_pattern: None,
base_path: PathBuf::new(), base_path: PathBuf::new(),
} }
}; };
@ -399,6 +463,7 @@ macro_rules! sync_rule {
include: Glob::new($pattern).unwrap(), include: Glob::new($pattern).unwrap(),
exclude: None, exclude: None,
suffix: Some($suffix.into()), suffix: Some($suffix.into()),
child_pattern: None,
base_path: PathBuf::new(), base_path: PathBuf::new(),
} }
}; };
@ -408,6 +473,7 @@ macro_rules! sync_rule {
include: Glob::new($pattern).unwrap(), include: Glob::new($pattern).unwrap(),
exclude: Some(Glob::new($exclude).unwrap()), exclude: Some(Glob::new($exclude).unwrap()),
suffix: Some($suffix.into()), suffix: Some($suffix.into()),
child_pattern: None,
base_path: PathBuf::new(), base_path: PathBuf::new(),
} }
}; };

View file

@ -1144,4 +1144,67 @@ mod test {
insta::assert_yaml_snapshot!(instance_snapshot); insta::assert_yaml_snapshot!(instance_snapshot);
} }
#[test]
fn sync_rule_child_pattern_dir() {
let _ = env_logger::try_init();
let mut imfs = InMemoryFs::new();
imfs.load_snapshot(
"/foo",
VfsSnapshot::dir([
(
"default.project.json",
VfsSnapshot::file(
r#"
{
"name": "child-pattern-test",
"syncRules": [
{
"pattern": "**/*.local.luau",
"child_pattern": "init.local.luau",
"use": "legacyClientScript"
}
],
"tree": {
"$path": "src"
}
}
"#,
),
),
(
"src",
VfsSnapshot::dir([(
"MyLocalComponent",
VfsSnapshot::dir([
("init.local.luau", VfsSnapshot::file("print('hello')")),
("ChildModule.luau", VfsSnapshot::file("return {}")),
]),
)]),
),
]),
)
.unwrap();
let vfs = Vfs::new(imfs);
let instance_snapshot = snapshot_project(
&InstanceContext::default(),
&vfs,
Path::new("/foo/default.project.json"),
"child_pattern_test",
)
.expect("snapshot error")
.expect("snapshot returned no instances");
assert_eq!(instance_snapshot.children.len(), 1);
let comp = &instance_snapshot.children[0];
assert_eq!(comp.name, "MyLocalComponent");
assert_eq!(comp.class_name, "LocalScript");
assert_eq!(comp.children.len(), 1);
let child = &comp.children[0];
assert_eq!(child.name, "ChildModule");
assert_eq!(child.class_name, "ModuleScript");
}
} }