diff --git a/examples/full_spec/task_engine/auto-yarn/composer.json b/examples/full_spec/task_engine/auto-yarn/composer.json new file mode 100644 index 0000000..8551144 --- /dev/null +++ b/examples/full_spec/task_engine/auto-yarn/composer.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "dev": "echo \"Hello from auto-yarn-composer\"" + } +} \ No newline at end of file diff --git a/examples/full_spec/task_engine/auto-yarn/package.json b/examples/full_spec/task_engine/auto-yarn/package.json new file mode 100644 index 0000000..678f543 --- /dev/null +++ b/examples/full_spec/task_engine/auto-yarn/package.json @@ -0,0 +1,6 @@ +{ + "license": "UNLICENSED", + "scripts": { + "dev": "echo \"Hello from auto-yarn-yarn\"" + } +} \ No newline at end of file diff --git a/examples/full_spec/task_engine/auto-yarn/rask.yaml b/examples/full_spec/task_engine/auto-yarn/rask.yaml new file mode 100644 index 0000000..ecf7cbd --- /dev/null +++ b/examples/full_spec/task_engine/auto-yarn/rask.yaml @@ -0,0 +1,3 @@ +name: composer + +task_engine: auto \ No newline at end of file diff --git a/examples/full_spec/task_engine/auto-yarn/yarn.lock b/examples/full_spec/task_engine/auto-yarn/yarn.lock new file mode 100644 index 0000000..e69de29 diff --git a/examples/full_spec/task_engine/auto/composer.json b/examples/full_spec/task_engine/auto/composer.json new file mode 100644 index 0000000..1f91dab --- /dev/null +++ b/examples/full_spec/task_engine/auto/composer.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "dev": "echo \"Hello from auto-composer\"" + } +} \ No newline at end of file diff --git a/examples/full_spec/task_engine/auto/package.json b/examples/full_spec/task_engine/auto/package.json new file mode 100644 index 0000000..4ad357f --- /dev/null +++ b/examples/full_spec/task_engine/auto/package.json @@ -0,0 +1,6 @@ +{ + "license": "UNLICENSED", + "scripts": { + "dev": "echo \"Hello from auto-npm\"" + } +} \ No newline at end of file diff --git a/examples/full_spec/task_engine/auto/rask.yaml b/examples/full_spec/task_engine/auto/rask.yaml new file mode 100644 index 0000000..ecf7cbd --- /dev/null +++ b/examples/full_spec/task_engine/auto/rask.yaml @@ -0,0 +1,3 @@ +name: composer + +task_engine: auto \ No newline at end of file diff --git a/examples/full_spec/task_engine/pnpm/package.json b/examples/full_spec/task_engine/composer/package.json similarity index 55% rename from examples/full_spec/task_engine/pnpm/package.json rename to examples/full_spec/task_engine/composer/package.json index 8c1d3ed..5ca305e 100644 --- a/examples/full_spec/task_engine/pnpm/package.json +++ b/examples/full_spec/task_engine/composer/package.json @@ -1,6 +1,6 @@ { "license": "UNLICENSED", "scripts": { - "dev": "echo \"Hello from pnpm\"" + "dev": "echo \"Hello from npm\"" } } \ No newline at end of file diff --git a/examples/full_spec/task_engine/pnpm/rask.yaml b/examples/full_spec/task_engine/pnpm/rask.yaml deleted file mode 100644 index ab8c52b..0000000 --- a/examples/full_spec/task_engine/pnpm/rask.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: pnpm - -task_engine: pnpm \ No newline at end of file diff --git a/src/commands/list.rs b/src/commands/list.rs index 98f0fd9..79e89a9 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -1,7 +1,8 @@ use std::path::PathBuf; use clap::Args; use crate::utils::config; -use crate::utils::config::{Config, ConfigFile}; +use crate::utils::config::{Config, ConfigTask}; +use crate::utils::file::ConfigFile; #[derive(Args, Debug)] pub struct Arguments { @@ -16,10 +17,10 @@ pub fn execute (arguments: &Arguments) -> Result<(), String> { let entry_config_path: PathBuf = config::resolve_config_path(entry)?; // Discover all config paths - let config_paths: Vec = config::discover_config_paths(&entry_config_path)?; + let config_file_paths: Vec = config::discover_config_paths(&entry_config_path)?; // Parse config file content - let config_files: Vec = config::read_config_files(config_paths)?; + let config_files: Vec = config::read_config_files(config_file_paths)?; // Parse config files let configs: Vec = config::parse_config_files(config_files)?; @@ -39,9 +40,10 @@ fn get_config_tasks(configs: &Vec) -> Result, String> { let mut tasks: Vec = vec![]; for config in configs { - for task in config.tasks.keys() { - if !tasks.contains(&task) { - tasks.push(task.clone()); + for configTask in &config.tasks { + let ConfigTask { key, .. } = configTask; + if !tasks.contains(&key) { + tasks.push(key.clone()); } } } diff --git a/src/commands/run.rs b/src/commands/run.rs index bfb55df..3b0185a 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -6,7 +6,8 @@ use std::thread::JoinHandle; use std::time::Instant; use clap::Args; use crate::utils::config; -use crate::utils::config::{Config, ConfigFile, ConfigStructure, OrderedTask, OrderedTasks, Task, TaskExit}; +use crate::utils::config::{Config, ConfigStructure, OrderedTask, OrderedTasks, Task, TaskExit}; +use crate::utils::file::ConfigFile; #[derive(Args, Debug)] pub struct Arguments { @@ -26,10 +27,10 @@ pub fn execute (arguments: &Arguments) -> Result<(), String> { let entry_config_path: PathBuf = config::resolve_config_path(entry)?; // Discover all config paths - let config_paths: Vec = config::discover_config_paths(&entry_config_path)?; + let config_file_paths: Vec = config::discover_config_paths(&entry_config_path)?; // Parse config file content - let config_files: Vec = config::read_config_files(config_paths)?; + let config_files: Vec = config::read_config_files(config_file_paths)?; // Parse config files let configs: Vec = config::parse_config_files(config_files)?; @@ -104,12 +105,13 @@ fn run_task_order(ordered_tasks: &OrderedTasks, order: u64) -> Result<(), String fn execute_task(task: &Task) -> Result, String> { let task_thread = thread::spawn({ - let task = task.clone(); + let Task { command, directory } = task.clone(); + println!("[COMMAND] {} @ {:?}", command, directory); move || { let status = Command::new("sh") .arg("-c") - .arg(&task.command) - .current_dir(&task.directory) + .arg(command) + .current_dir(directory) .status() .expect("Failed to execute command"); status.success() diff --git a/src/utils/config.rs b/src/utils/config.rs index eba14e6..62ad383 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -1,9 +1,11 @@ use std::path::{Path, PathBuf}; use std::fmt::Debug; -use std::fs::{canonicalize, read_to_string}; +use std::fs::canonicalize; use std::collections::HashMap; use globset::{Glob, GlobSetBuilder}; use serde::Deserialize; +use crate::utils::file; +use crate::utils::file::{ConfigFile, ConfigFileTasks, TaskEngine}; #[derive(Debug, Clone)] pub enum TaskExit { @@ -36,15 +38,18 @@ pub fn resolve_task_order(config_structure: ConfigStructure, task_name: &String) fn order_tasks(ordered_tasks: &mut OrderedTasks, config_structure: ConfigStructure, task_name: &String, index: u64) { let ConfigStructure { config, children } = config_structure; let Config { tasks, dir_path, .. } = config; - match tasks.get(task_name) { - None => {} - Some(&ref config_task) => ordered_tasks.push(OrderedTask{ - task: Task { - command: config_task.content.clone(), - directory: dir_path, - }, - order: index - }) + + for config_task in tasks { + let ConfigTask { ref key, .. } = config_task; + if key == task_name { + ordered_tasks.push(OrderedTask { + task: Task { + command: resolve_config_task_command(&config_task.clone()), + directory: dir_path.clone(), // compiler says it's being moved, No idea where... + }, + order: index + }) + } } for child in children { @@ -107,45 +112,44 @@ fn construct_config_structure(config_path: &PathBuf, config_path_map: &HashMap

; +pub fn resolve_config_task_command(config_task: &ConfigTask) -> String { + let ConfigTask { task_type, key, value } = config_task; + + match task_type { + TaskType::SHELL => value.clone(), + TaskType::COMPOSER => format!("composer run {}", key), + TaskType::NPM => format!("npm run {}", key), + TaskType::YARN => format!("yarn run {}", key), + } +} + +type ConfigTasks = Vec; type ConfigDirectories = Vec; -#[derive(Debug, Clone, Default, Deserialize)] -pub enum TaskEngine { - #[serde(rename = "composer")] - COMPOSER, - #[serde(rename = "npm")] - NPM, - #[serde(rename = "yarn")] - YARN, - #[serde(rename = "pnpm")] - PNPM, - #[serde(rename = "none")] - #[default] - NONE -} - #[derive(Debug, Clone)] -#[allow(dead_code)] pub struct Config { - pub(crate)name: String, - pub(crate)task_engine: TaskEngine, - pub(crate)tasks: HashMap, - pub(crate)file_path: PathBuf, - pub(crate)dir_path: PathBuf, - pub(crate)directories: ConfigDirectories, + pub(crate) name: String, + pub(crate) tasks: ConfigTasks, + pub(crate) file_path: PathBuf, + pub(crate) dir_path: PathBuf, + pub(crate) directories: ConfigDirectories, } pub fn parse_config_files(config_files: Vec) -> Result, String> { @@ -160,39 +164,72 @@ pub fn parse_config_files(config_files: Vec) -> Result, } fn parse_config_file(config_file: ConfigFile) -> Result { - let ConfigFile { name, tasks: config_file_tasks, __file_path: file_path, __dir_path: dir_path, directories, task_engine } = config_file; + let ConfigFile { name, directories, task_engine, tasks: config_file_tasks, .. } = config_file; + let ConfigFile { __file_path: file_path, __dir_path: dir_path, .. } = config_file; let tasks: ConfigTasks = match task_engine { - TaskEngine::COMPOSER => parse_composer_tasks(&dir_path)?, - TaskEngine::NPM => parse_node_tasks(&dir_path, "npm")?, - TaskEngine::YARN => parse_node_tasks(&dir_path, "yarn")?, - TaskEngine::PNPM => parse_node_tasks(&dir_path, "pnpm")?, + TaskEngine::COMPOSER => parse_composer_json_tasks(&dir_path)?, + TaskEngine::NPM => parse_package_json_tasks(&dir_path, TaskType::NPM)?, + TaskEngine::YARN => parse_package_json_tasks(&dir_path, TaskType::YARN)?, TaskEngine::NONE => parse_config_tasks(config_file_tasks)?, + TaskEngine::AUTO => parse_discovered_tasks(&dir_path)?, }; - let config: Config = Config { name, tasks, file_path, dir_path, directories, task_engine }; + let config: Config = Config { name, tasks, file_path, dir_path, directories }; Ok(config) } +const PACKAGE_JSON_FILE: &str = "package.json"; +const NPM_LOCK_FILE: &str = "package.lock"; +const YARN_LOCK_FILE: &str = "yarn.lock"; +const COMPOSER_JSON_FILE: &str = "composer.json"; + +fn parse_discovered_tasks(dir_path: &PathBuf) -> Result { + let mut config_tasks: ConfigTasks = vec![]; + + // Gathering facts + let has_composer_json = dir_path.join(COMPOSER_JSON_FILE).exists(); + let has_package_json = dir_path.join(PACKAGE_JSON_FILE).exists(); + let has_yarn_lock = dir_path.join(YARN_LOCK_FILE).exists(); + + + if has_composer_json { + let composer_config_tasks = parse_composer_json_tasks(dir_path)?; + config_tasks.extend(composer_config_tasks.into_iter()) + } + + if has_package_json { + let mut package_config_tasks: ConfigTasks; + + if has_yarn_lock { + package_config_tasks = parse_package_json_tasks(dir_path, TaskType::YARN)?; + } else { + // No lock file, for now we assume the uses intends to use NPM. + package_config_tasks = parse_package_json_tasks(dir_path, TaskType::NPM)?; + } + + config_tasks.extend(package_config_tasks); + } + + Ok(config_tasks) +} + #[derive(Debug, Clone, Deserialize, Default)] struct PackageJsonFile { #[serde(default)] scripts: HashMap, } -fn parse_node_tasks(dir_path: &PathBuf, prefix: &str) -> Result { - let mut file_path = dir_path.clone(); - file_path.push("package.json"); - let content = read_file_content(file_path)?; +fn parse_package_json_tasks(dir_path: &PathBuf, task_type: TaskType) -> Result { + let package_json = file::read_json_file::(&dir_path.join(PACKAGE_JSON_FILE))?; - let package_json: PackageJsonFile = serde_json::from_str(&content).expect(format!("Failed to package.json from \"{:?}\"", dir_path).as_str()); - - let mut config_tasks: ConfigTasks = HashMap::new(); + let mut config_tasks: ConfigTasks = vec![]; for key in package_json.scripts.keys() { - config_tasks.insert(key.clone(), ConfigTask { - task_type: TaskType::SHELL, - content: format!("{:?} run {:?}", prefix, key) + config_tasks.push(ConfigTask { + task_type, + key: key.clone(), + value: key.clone() }); } @@ -212,18 +249,15 @@ struct ComposerJsonFile { scripts: HashMap, } -fn parse_composer_tasks(dir_path: &PathBuf) -> Result { - let mut file_path = dir_path.clone(); - file_path.push("composer.json"); - let content = read_file_content(file_path)?; +fn parse_composer_json_tasks(dir_path: &PathBuf) -> Result { + let package_json = file::read_json_file::(&dir_path.join(COMPOSER_JSON_FILE))?; - let package_json: ComposerJsonFile = serde_json::from_str(&content).expect(format!("Failed to composer.json from \"{:?}\"", dir_path).as_str()); - - let mut config_tasks: ConfigTasks = HashMap::new(); + let mut config_tasks: ConfigTasks = vec![]; for key in package_json.scripts.keys() { - config_tasks.insert(key.clone(), ConfigTask { - task_type: TaskType::SHELL, - content: format!("composer run {:?}", key) + config_tasks.push(ConfigTask { + task_type: TaskType::COMPOSER, + key: key.clone(), + value: key.clone(), }); } @@ -231,25 +265,24 @@ fn parse_composer_tasks(dir_path: &PathBuf) -> Result { } fn parse_config_tasks(tasks: ConfigFileTasks) -> Result { - let mut config_tasks: ConfigTasks = HashMap::new(); + let mut config_tasks: ConfigTasks = vec![]; for (key, value) in tasks { - let config_task: ConfigTask = ConfigTask { + config_tasks.push(ConfigTask { task_type: TaskType::SHELL, - content: value - }; - - config_tasks.insert(key, config_task); + key, + value + }); } Ok(config_tasks) } -pub fn read_config_files(paths: Vec) -> Result, String> { +pub fn read_config_files(config_file_paths: Vec) -> Result, String> { let mut configs_files: Vec = vec![]; - for path in paths { - let config_file = read_config_file(path)?; + for config_file_path in config_file_paths { + let config_file = file::read_config_file(config_file_path)?; configs_files.push(config_file); } @@ -262,7 +295,7 @@ pub fn discover_config_paths(path: &PathBuf) -> Result, String> { // Read config let mut path_stack: Vec = vec![path.clone()]; while !path_stack.is_empty() { - let ConfigFile { directories, __file_path: _file_path, .. } = read_config_file(path_stack.pop().unwrap())?; + let ConfigFile { directories, __file_path: _file_path, .. } = file::read_config_file(path_stack.pop().unwrap())?; // Extract directories let config_directory = _file_path.parent().ok_or("Failed to get parent directory")?; @@ -297,42 +330,6 @@ fn get_config_glob_pattern(root_path: &Path, glob_pattern: &String) -> PathBuf { pattern } -fn read_file_content (path: PathBuf) -> Result { - match read_to_string(path) { - Ok(content) => Ok(content), - Err(err) => Err(format!("Failed to read file: {}", err)), - } -} - -type ConfigFileTasks = HashMap; - -#[derive(Debug, Deserialize, Default)] -pub struct ConfigFile { - pub(crate) name: String, - #[serde(default)] - pub(crate) task_engine: TaskEngine, - #[serde(default)] - pub(crate) directories: ConfigDirectories, - #[serde(default)] - pub(crate) tasks: ConfigFileTasks, - // The following fields are not part of the yaml file. - #[serde(default)] - __file_path: PathBuf, - #[serde(default)] - __dir_path: PathBuf, -} - -fn read_config_file(path: PathBuf) -> Result { - let content = read_file_content(path.clone())?; - - let mut config_file: ConfigFile = serde_yaml::from_str(&content).expect(format!("Failed to parse YAML from \"{:?}\"", path).as_str()); - - config_file.__file_path = path.clone(); - config_file.__dir_path = path.parent().unwrap().to_path_buf(); - - Ok(config_file) -} - pub fn resolve_config_path + Debug + Clone + Copy>(path: P) -> Result { let full_path = match canonicalize(path) { Ok(full_path) => full_path, @@ -355,8 +352,7 @@ fn find_config_file(directory_path: PathBuf) -> Result { } for filename in CONFIG_FILENAMES { - let mut possible_config_file = directory_path.clone(); - possible_config_file.push(filename); + let possible_config_file = directory_path.join(filename); match possible_config_file.exists() { true => return Ok(possible_config_file), false => {} diff --git a/src/utils/file.rs b/src/utils/file.rs new file mode 100644 index 0000000..09f1eec --- /dev/null +++ b/src/utils/file.rs @@ -0,0 +1,70 @@ +use std::path::PathBuf; +use std::fs::read_to_string; +use std::collections::HashMap; +use serde::Deserialize; + +pub fn read_file_content (path: PathBuf) -> Result { + match read_to_string(path) { + Ok(content) => Ok(content), + Err(err) => Err(format!("Failed to read file: {}", err)), + } +} + +pub fn read_json_file Deserialize<'a>>(file_path: &PathBuf) -> Result { + let content = read_file_content(file_path.clone())?; + + let file_content: T = serde_json::from_str::(&content).expect(format!("Failed to read the file: \"{:?}\"", file_path).as_str()); + + Ok(file_content) +} + +fn read_yaml_file Deserialize<'a>>(file_path: &PathBuf) -> Result { + let content = read_file_content(file_path.clone())?; + + let file_content: T = serde_yaml::from_str::(&content).expect(format!("Failed to read the file: \"{:?}\"", file_path).as_str()); + + Ok(file_content) +} + +#[derive(Debug, Clone, Default, Deserialize)] +pub enum TaskEngine { + #[serde(rename = "composer")] + COMPOSER, + #[serde(rename = "npm")] + NPM, + #[serde(rename = "yarn")] + YARN, + #[serde(rename = "none")] + NONE, + #[serde(rename = "auto")] + #[default] + AUTO, +} + +pub type ConfigFileTasks = HashMap; + +#[derive(Debug, Deserialize, Clone, Default)] +pub struct ConfigFile { + pub(crate) name: String, + #[serde(default)] + pub(crate) task_engine: TaskEngine, + #[serde(default)] + pub(crate) directories: Vec, + #[serde(default)] + pub(crate) tasks: ConfigFileTasks, + // The following fields are not part of the yaml file. + #[serde(default)] + pub(crate) __file_path: PathBuf, + #[serde(default)] + pub(crate) __dir_path: PathBuf, +} + +pub fn read_config_file(config_file_path: PathBuf) -> Result { + let mut config_file = read_yaml_file::(&config_file_path)?; + + config_file.__file_path = config_file_path.clone(); + config_file.__dir_path = config_file_path.parent().unwrap().to_path_buf(); + + Ok(config_file) +} + diff --git a/src/utils/mod.rs b/src/utils/mod.rs index ef68c36..793bf6a 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1 +1,2 @@ pub mod config; +pub mod file;