mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-20 17:41:37 +00:00
feat: add child_pattern support to sync rules for directory entry points
This commit is contained in:
parent
a30a8ecd0f
commit
cd82b3b00a
6 changed files with 220 additions and 17 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -73,14 +73,11 @@ 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) =
|
||||||
// TODO: Support user defined init paths
|
get_dir_middleware(vfs, path, &context.sync_rules)?;
|
||||||
// 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 +94,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 +119,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 +159,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 +369,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 +427,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 +454,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 +464,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 +474,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(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue