diff --git a/CHANGELOG.md b/CHANGELOG.md index ef386b6..e88c94f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Breaking Changes + +- The Lune CLI now uses subcommands instead of flag options:
+ + - `lune script_name arg1 arg2 arg3` -> `lune run script_name arg1 arg2 arg3` + - `lune --list` -> `lune list` + - `lune --setup` -> `lune setup` + + This unfortunately hurts ergonomics for quickly running scripts but is a necessary change to allow us to add more commands, such as the new `build` subcommand. + ### Added +- Added support for compiling single Lune scripts into standalone executables! ([#140]) + + Example usage: + + ```lua + -- my_cool_script.luau + print("Hello, standalone!") + ``` + + ```sh + > lune build my_cool_script + # Creates `my_cool_script.exe` (Windows) or `my_cool_script` (macOS / Linux) + ``` + + ```sh + > ./my_cool_script.exe # Windows + > ./my_cool_script # macOS / Linux + > "Hello, standalone!" + ``` + + To compile scripts that use `require` and reference multiple files, a bundler such as [darklua](https://github.com/seaofvoices/darklua) will need to be used first. This limitation will be lifted in the future and Lune will automatically bundle any referenced scripts. + - Added support for multiple values for a single query, and multiple values for a single header, in `net.request`. This is a part of the HTTP specification that is not widely used but that may be useful in certain cases. To clarify: - Single values remain unchanged and will work exactly the same as before.
@@ -42,6 +74,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 }) ``` +[#140]: https://github.com/filiptibell/lune/pull/140 + ### Changed - Update to Luau version `0.606`. diff --git a/src/cli/build.rs b/src/cli/build.rs index 998077a..74a7945 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -1,51 +1,70 @@ -use std::{env, path::Path, process::ExitCode}; +use std::{ + env::{self, consts::EXE_EXTENSION}, + path::{Path, PathBuf}, + process::ExitCode, +}; -use anyhow::Result; +use anyhow::{Context, Result}; +use clap::Parser; use console::style; use mlua::Compiler as LuaCompiler; use tokio::{fs, io::AsyncWriteExt as _}; use crate::executor::MetaChunk; -/** - Compiles and embeds the bytecode of a given lua file to form a standalone - binary, then writes it to an output file, with the required permissions. -*/ -#[allow(clippy::similar_names)] -pub async fn build_standalone( - input_path: impl AsRef, - output_path: impl AsRef, - source_code: impl AsRef<[u8]>, -) -> Result { - let input_path_displayed = input_path.as_ref().display(); - let output_path_displayed = output_path.as_ref().display(); +/// Build a standalone executable +#[derive(Debug, Clone, Parser)] +pub struct BuildCommand { + /// The path to the input file + pub input: PathBuf, - // First, we read the contents of the lune interpreter as our starting point - println!( - "Creating standalone binary using {}", - style(input_path_displayed).green() - ); - let mut patched_bin = fs::read(env::current_exe()?).await?; + /// The path to the output file - defaults to the + /// input file path with an executable extension + #[clap(short, long)] + pub output: Option, +} - // Compile luau input into bytecode - let bytecode = LuaCompiler::new() - .set_optimization_level(2) - .set_coverage_level(0) - .set_debug_level(1) - .compile(source_code); +impl BuildCommand { + pub async fn run(self) -> Result { + let output_path = self + .output + .unwrap_or_else(|| self.input.with_extension(EXE_EXTENSION)); - // Append the bytecode / metadata to the end - let meta = MetaChunk { bytecode }; - patched_bin.extend_from_slice(&meta.to_bytes()); + let input_path_displayed = self.input.display(); + let output_path_displayed = output_path.display(); - // And finally write the patched binary to the output file - println!( - "Writing standalone binary to {}", - style(output_path_displayed).blue() - ); - write_executable_file_to(output_path, patched_bin).await?; + // Try to read the input file + let source_code = fs::read(&self.input) + .await + .context("failed to read input file")?; - Ok(ExitCode::SUCCESS) + // Read the contents of the lune interpreter as our starting point + println!( + "Creating standalone binary using {}", + style(input_path_displayed).green() + ); + let mut patched_bin = fs::read(env::current_exe()?).await?; + + // Compile luau input into bytecode + let bytecode = LuaCompiler::new() + .set_optimization_level(2) + .set_coverage_level(0) + .set_debug_level(1) + .compile(source_code); + + // Append the bytecode / metadata to the end + let meta = MetaChunk { bytecode }; + patched_bin.extend_from_slice(&meta.to_bytes()); + + // And finally write the patched binary to the output file + println!( + "Writing standalone binary to {}", + style(output_path_displayed).blue() + ); + write_executable_file_to(output_path, patched_bin).await?; + + Ok(ExitCode::SUCCESS) + } } async fn write_executable_file_to(path: impl AsRef, bytes: impl AsRef<[u8]>) -> Result<()> { diff --git a/src/cli/gen.rs b/src/cli/gen.rs deleted file mode 100644 index eb6268b..0000000 --- a/src/cli/gen.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::collections::HashMap; - -use anyhow::{Context, Result}; -use directories::UserDirs; -use futures_util::future::try_join_all; -use include_dir::Dir; -use tokio::fs::{create_dir_all, write}; - -pub async fn generate_typedef_files_from_definitions(dir: &Dir<'_>) -> Result { - let contents = read_typedefs_dir_contents(dir); - write_typedef_files(contents).await -} - -fn read_typedefs_dir_contents(dir: &Dir<'_>) -> HashMap> { - let mut definitions = HashMap::new(); - - for entry in dir.find("*.luau").unwrap() { - let entry_file = entry.as_file().unwrap(); - let entry_name = entry_file.path().file_name().unwrap().to_string_lossy(); - - let typedef_name = entry_name.trim_end_matches(".luau"); - let typedef_contents = entry_file.contents().to_vec(); - - definitions.insert(typedef_name.to_string(), typedef_contents); - } - - definitions -} - -async fn write_typedef_files(typedef_files: HashMap>) -> Result { - let version_string = env!("CARGO_PKG_VERSION"); - let mut dirs_to_write = Vec::new(); - let mut files_to_write = Vec::new(); - // Create the typedefs dir in the users cache dir - let cache_dir = UserDirs::new() - .context("Failed to find user home directory")? - .home_dir() - .join(".lune") - .join(".typedefs") - .join(version_string); - dirs_to_write.push(cache_dir.clone()); - // Make typedef files - for (builtin_name, builtin_typedef) in typedef_files { - let path = cache_dir - .join(builtin_name.to_ascii_lowercase()) - .with_extension("luau"); - files_to_write.push((builtin_name.to_lowercase(), path, builtin_typedef)); - } - // Write all dirs and files only when we know generation was successful - let futs_dirs = dirs_to_write - .drain(..) - .map(create_dir_all) - .collect::>(); - let futs_files = files_to_write - .iter() - .map(|(_, path, contents)| write(path, contents)) - .collect::>(); - try_join_all(futs_dirs).await?; - try_join_all(futs_files).await?; - Ok(version_string.to_string()) -} diff --git a/src/cli/list.rs b/src/cli/list.rs new file mode 100644 index 0000000..48b5248 --- /dev/null +++ b/src/cli/list.rs @@ -0,0 +1,47 @@ +use std::{fmt::Write as _, process::ExitCode}; + +use anyhow::Result; +use clap::Parser; + +use super::utils::listing::{find_lune_scripts, sort_lune_scripts, write_lune_scripts_list}; + +/// List scripts available to run +#[derive(Debug, Clone, Parser)] +pub struct ListCommand {} + +impl ListCommand { + pub async fn run(self) -> Result { + let sorted_relative = find_lune_scripts(false).await.map(sort_lune_scripts); + + let sorted_home_dir = find_lune_scripts(true).await.map(sort_lune_scripts); + if sorted_relative.is_err() && sorted_home_dir.is_err() { + eprintln!("{}", sorted_relative.unwrap_err()); + return Ok(ExitCode::FAILURE); + } + + let sorted_relative = sorted_relative.unwrap_or(Vec::new()); + let sorted_home_dir = sorted_home_dir.unwrap_or(Vec::new()); + + let mut buffer = String::new(); + if !sorted_relative.is_empty() { + if sorted_home_dir.is_empty() { + write!(&mut buffer, "Available scripts:")?; + } else { + write!(&mut buffer, "Available scripts in current directory:")?; + } + write_lune_scripts_list(&mut buffer, sorted_relative)?; + } + if !sorted_home_dir.is_empty() { + write!(&mut buffer, "Available global scripts:")?; + write_lune_scripts_list(&mut buffer, sorted_home_dir)?; + } + + if buffer.is_empty() { + println!("No scripts found."); + } else { + print!("{buffer}"); + } + + Ok(ExitCode::SUCCESS) + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 6c8b276..2e050ad 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,180 +1,54 @@ -use std::{env, fmt::Write as _, path::PathBuf, process::ExitCode}; +use std::process::ExitCode; -use anyhow::{Context, Result}; -use clap::Parser; - -use lune::Lune; -use tokio::{ - fs::read as read_to_vec, - io::{stdin, AsyncReadExt}, -}; +use anyhow::Result; +use clap::{Parser, Subcommand}; pub(crate) mod build; -pub(crate) mod gen; +pub(crate) mod list; pub(crate) mod repl; +pub(crate) mod run; pub(crate) mod setup; pub(crate) mod utils; -use setup::run_setup; -use utils::{ - files::{discover_script_path_including_lune_dirs, strip_shebang}, - listing::{find_lune_scripts, sort_lune_scripts, write_lune_scripts_list}, +pub use self::{ + build::BuildCommand, list::ListCommand, repl::ReplCommand, run::RunCommand, setup::SetupCommand, }; -use self::build::build_standalone; - -/// A Luau script runner -#[derive(Parser, Debug, Default, Clone)] -#[command(version, long_about = None)] -pub struct Cli { - /// Script name or full path to the file to run - script_path: Option, - /// Arguments to pass to the script, stored in process.args - script_args: Vec, - /// List scripts found inside of a nearby `lune` directory - #[clap(long, short = 'l')] - list: bool, - /// Set up type definitions and settings for development - #[clap(long)] - setup: bool, - /// Build a Luau file to an OS-Native standalone executable - #[clap(long)] - build: bool, +#[derive(Debug, Clone, Subcommand)] +pub enum CliSubcommand { + Run(RunCommand), + List(ListCommand), + Setup(SetupCommand), + Build(BuildCommand), + Repl(ReplCommand), +} + +impl Default for CliSubcommand { + fn default() -> Self { + Self::Repl(ReplCommand::default()) + } +} + +/// Lune, a standalone Luau runtime +#[derive(Parser, Debug, Default, Clone)] +#[command(version, about, long_about = None)] +pub struct Cli { + #[clap(subcommand)] + subcommand: Option, } -#[allow(dead_code)] impl Cli { pub fn new() -> Self { - Self::default() + Self::parse() } - pub fn with_path(mut self, path: S) -> Self - where - S: Into, - { - self.script_path = Some(path.into()); - self - } - - pub fn with_args(mut self, args: A) -> Self - where - A: Into>, - { - self.script_args = args.into(); - self - } - - pub fn setup(mut self) -> Self { - self.setup = true; - self - } - - pub fn list(mut self) -> Self { - self.list = true; - self - } - - #[allow(clippy::too_many_lines)] pub async fn run(self) -> Result { - // List files in `lune` and `.lune` directories, if wanted - // This will also exit early and not run anything else - if self.list { - let sorted_relative = find_lune_scripts(false).await.map(sort_lune_scripts); - - let sorted_home_dir = find_lune_scripts(true).await.map(sort_lune_scripts); - if sorted_relative.is_err() && sorted_home_dir.is_err() { - eprintln!("{}", sorted_relative.unwrap_err()); - return Ok(ExitCode::FAILURE); - } - - let sorted_relative = sorted_relative.unwrap_or(Vec::new()); - let sorted_home_dir = sorted_home_dir.unwrap_or(Vec::new()); - - let mut buffer = String::new(); - if !sorted_relative.is_empty() { - if sorted_home_dir.is_empty() { - write!(&mut buffer, "Available scripts:")?; - } else { - write!(&mut buffer, "Available scripts in current directory:")?; - } - write_lune_scripts_list(&mut buffer, sorted_relative)?; - } - if !sorted_home_dir.is_empty() { - write!(&mut buffer, "Available global scripts:")?; - write_lune_scripts_list(&mut buffer, sorted_home_dir)?; - } - - if buffer.is_empty() { - println!("No scripts found."); - } else { - print!("{buffer}"); - } - - return Ok(ExitCode::SUCCESS); + match self.subcommand.unwrap_or_default() { + CliSubcommand::Run(cmd) => cmd.run().await, + CliSubcommand::List(cmd) => cmd.run().await, + CliSubcommand::Setup(cmd) => cmd.run().await, + CliSubcommand::Build(cmd) => cmd.run().await, + CliSubcommand::Repl(cmd) => cmd.run().await, } - - // Generate (save) definition files, if wanted - if self.setup { - run_setup().await; - } - if self.script_path.is_none() { - // Only generating typedefs without running a script is completely - // fine, and we should just exit the program normally afterwards - if self.setup { - return Ok(ExitCode::SUCCESS); - } - - // If not in a standalone context and we don't have any arguments - // display the interactive REPL interface - return repl::show_interface().await; - } - - // Figure out if we should read from stdin or from a file, - // reading from stdin is marked by passing a single "-" - // (dash) as the script name to run to the cli - let script_path = self.script_path.unwrap(); - - let (script_display_name, script_contents) = if script_path == "-" { - let mut stdin_contents = Vec::new(); - stdin() - .read_to_end(&mut stdin_contents) - .await - .context("Failed to read script contents from stdin")?; - ("stdin".to_string(), stdin_contents) - } else { - let file_path = discover_script_path_including_lune_dirs(&script_path)?; - let file_contents = read_to_vec(&file_path).await?; - // NOTE: We skip the extension here to remove it from stack traces - let file_display_name = file_path.with_extension("").display().to_string(); - (file_display_name, file_contents) - }; - - if self.build { - let output_path = - PathBuf::from(script_path.clone()).with_extension(env::consts::EXE_EXTENSION); - - return Ok( - match build_standalone(script_path, output_path, script_contents).await { - Ok(exitcode) => exitcode, - Err(err) => { - eprintln!("{err}"); - ExitCode::FAILURE - } - }, - ); - } - - // Create a new lune object with all globals & run the script - let result = Lune::new() - .with_args(self.script_args) - .run(&script_display_name, strip_shebang(script_contents)) - .await; - Ok(match result { - Err(err) => { - eprintln!("{err}"); - ExitCode::FAILURE - } - Ok(code) => code, - }) } } diff --git a/src/cli/repl.rs b/src/cli/repl.rs index 71b4923..915a314 100644 --- a/src/cli/repl.rs +++ b/src/cli/repl.rs @@ -1,6 +1,7 @@ use std::{path::PathBuf, process::ExitCode}; use anyhow::{Context, Result}; +use clap::Parser; use directories::UserDirs; use rustyline::{error::ReadlineError, DefaultEditor}; @@ -14,87 +15,93 @@ enum PromptState { Continuation, } -pub async fn show_interface() -> Result { - println!("{MESSAGE_WELCOME}"); +/// Launch an interactive REPL (default) +#[derive(Debug, Clone, Default, Parser)] +pub struct ReplCommand {} - let history_file_path: &PathBuf = &UserDirs::new() - .context("Failed to find user home directory")? - .home_dir() - .join(".lune_history"); - if !history_file_path.exists() { - tokio::fs::write(history_file_path, &[]).await?; +impl ReplCommand { + pub async fn run(self) -> Result { + println!("{MESSAGE_WELCOME}"); + + let history_file_path: &PathBuf = &UserDirs::new() + .context("Failed to find user home directory")? + .home_dir() + .join(".lune_history"); + if !history_file_path.exists() { + tokio::fs::write(history_file_path, &[]).await?; + } + + let mut repl = DefaultEditor::new()?; + repl.load_history(history_file_path)?; + + let mut interrupt_counter = 0; + let mut prompt_state = PromptState::Regular; + let mut source_code = String::new(); + + let mut lune_instance = Lune::new(); + + loop { + let prompt = match prompt_state { + PromptState::Regular => "> ", + PromptState::Continuation => ">> ", + }; + + match repl.readline(prompt) { + Ok(code) => { + interrupt_counter = 0; + + // TODO: Should we add history entries for each separate line? + // Or should we add and save history only when we have complete + // lua input that may or may not be multiple lines long? + repl.add_history_entry(&code)?; + repl.save_history(history_file_path)?; + + match prompt_state { + PromptState::Regular => source_code = code, + PromptState::Continuation => source_code.push_str(&code), + } + } + + Err(ReadlineError::Eof) => break, + Err(ReadlineError::Interrupted) => { + interrupt_counter += 1; + + // NOTE: We actually want the user to do ^C twice to exit, + // and if we get an interrupt we should continue to the next + // readline loop iteration so we don't run input code twice + if interrupt_counter == 1 { + println!("{MESSAGE_INTERRUPT}"); + continue; + } + + break; + } + + Err(err) => { + eprintln!("REPL ERROR: {err}"); + return Ok(ExitCode::FAILURE); + } + }; + + // TODO: Preserve context here somehow? + let eval_result = lune_instance.run("REPL", &source_code).await; + + match eval_result { + Ok(_) => prompt_state = PromptState::Regular, + + Err(err) => { + if err.is_incomplete_input() { + prompt_state = PromptState::Continuation; + source_code.push('\n'); + } else { + eprintln!("{err}"); + } + } + }; + } + + repl.save_history(history_file_path)?; + + Ok(ExitCode::SUCCESS) } - - let mut repl = DefaultEditor::new()?; - repl.load_history(history_file_path)?; - - let mut interrupt_counter = 0; - let mut prompt_state = PromptState::Regular; - let mut source_code = String::new(); - - let mut lune_instance = Lune::new(); - - loop { - let prompt = match prompt_state { - PromptState::Regular => "> ", - PromptState::Continuation => ">> ", - }; - - match repl.readline(prompt) { - Ok(code) => { - interrupt_counter = 0; - - // TODO: Should we add history entries for each separate line? - // Or should we add and save history only when we have complete - // lua input that may or may not be multiple lines long? - repl.add_history_entry(&code)?; - repl.save_history(history_file_path)?; - - match prompt_state { - PromptState::Regular => source_code = code, - PromptState::Continuation => source_code.push_str(&code), - } - } - - Err(ReadlineError::Eof) => break, - Err(ReadlineError::Interrupted) => { - interrupt_counter += 1; - - // NOTE: We actually want the user to do ^C twice to exit, - // and if we get an interrupt we should continue to the next - // readline loop iteration so we don't run input code twice - if interrupt_counter == 1 { - println!("{MESSAGE_INTERRUPT}"); - continue; - } - - break; - } - - Err(err) => { - eprintln!("REPL ERROR: {err}"); - return Ok(ExitCode::FAILURE); - } - }; - - // TODO: Preserve context here somehow? - let eval_result = lune_instance.run("REPL", &source_code).await; - - match eval_result { - Ok(_) => prompt_state = PromptState::Regular, - - Err(err) => { - if err.is_incomplete_input() { - prompt_state = PromptState::Continuation; - source_code.push('\n'); - } else { - eprintln!("{err}"); - } - } - }; - } - - repl.save_history(history_file_path)?; - - Ok(ExitCode::SUCCESS) } diff --git a/src/cli/run.rs b/src/cli/run.rs new file mode 100644 index 0000000..e5877a7 --- /dev/null +++ b/src/cli/run.rs @@ -0,0 +1,56 @@ +use std::process::ExitCode; + +use anyhow::{Context, Result}; +use clap::Parser; +use tokio::{ + fs::read as read_to_vec, + io::{stdin, AsyncReadExt as _}, +}; + +use lune::Lune; + +use super::utils::files::{discover_script_path_including_lune_dirs, strip_shebang}; + +/// Run a script +#[derive(Debug, Clone, Parser)] +pub struct RunCommand { + /// Script name or full path to the file to run + script_path: String, + /// Arguments to pass to the script, stored in process.args + script_args: Vec, +} + +impl RunCommand { + pub async fn run(self) -> Result { + // Figure out if we should read from stdin or from a file, + // reading from stdin is marked by passing a single "-" + // (dash) as the script name to run to the cli + let (script_display_name, script_contents) = if &self.script_path == "-" { + let mut stdin_contents = Vec::new(); + stdin() + .read_to_end(&mut stdin_contents) + .await + .context("Failed to read script contents from stdin")?; + ("stdin".to_string(), stdin_contents) + } else { + let file_path = discover_script_path_including_lune_dirs(&self.script_path)?; + let file_contents = read_to_vec(&file_path).await?; + // NOTE: We skip the extension here to remove it from stack traces + let file_display_name = file_path.with_extension("").display().to_string(); + (file_display_name, file_contents) + }; + + // Create a new lune object with all globals & run the script + let result = Lune::new() + .with_args(self.script_args) + .run(&script_display_name, strip_shebang(script_contents)) + .await; + Ok(match result { + Err(err) => { + eprintln!("{err}"); + ExitCode::FAILURE + } + Ok(code) => code, + }) + } +} diff --git a/src/cli/setup.rs b/src/cli/setup.rs index 51a23c8..4b25e9a 100644 --- a/src/cli/setup.rs +++ b/src/cli/setup.rs @@ -1,6 +1,12 @@ -use std::{borrow::BorrowMut, env::current_dir, io::ErrorKind, path::PathBuf}; +use std::{ + borrow::BorrowMut, collections::HashMap, env::current_dir, io::ErrorKind, path::PathBuf, + process::ExitCode, +}; -use anyhow::Result; +use anyhow::{Context, Result}; +use clap::Parser; +use directories::UserDirs; +use futures_util::future::try_join_all; use include_dir::{include_dir, Dir}; use thiserror::Error; use tokio::fs; @@ -8,13 +14,50 @@ use tokio::fs; // TODO: Use a library that supports json with comments since VSCode settings may contain comments use serde_json::Value as JsonValue; -use super::gen::generate_typedef_files_from_definitions; - pub(crate) static TYPEDEFS_DIR: Dir<'_> = include_dir!("types"); pub(crate) static SETTING_NAME_MODE: &str = "luau-lsp.require.mode"; pub(crate) static SETTING_NAME_ALIASES: &str = "luau-lsp.require.directoryAliases"; +/// Set up type definitions for your editor +#[derive(Debug, Clone, Parser)] +pub struct SetupCommand {} + +impl SetupCommand { + pub async fn run(self) -> Result { + generate_typedef_files_from_definitions(&TYPEDEFS_DIR) + .await + .expect("Failed to generate typedef files"); + + // TODO: Let the user interactively choose what editor to set up + let res = async { + let settings = read_or_create_vscode_settings_json().await?; + let modified = add_values_to_vscode_settings_json(settings); + write_vscode_settings_json(modified).await?; + Ok::<_, SetupError>(()) + } + .await; + + let message = match res { + Ok(()) => "These settings have been added to your workspace for Visual Studio Code:", + Err(_) => "To finish setting up your editor, add these settings to your workspace:", + }; + + let version_string = lune_version(); + println!( + "Lune has now been set up and editor type definitions have been generated.\ + \n{message}\ + \n\ + \n\"{SETTING_NAME_MODE}\": \"relativeToFile\",\ + \n\"{SETTING_NAME_ALIASES}\": {{\ + \n \"@lune/\": \"~/.lune/.typedefs/{version_string}/\"\ + \n}}", + ); + + Ok(ExitCode::SUCCESS) + } +} + #[derive(Debug, Clone, Copy, Error)] enum SetupError { #[error("Failed to read settings")] @@ -99,30 +142,56 @@ fn add_values_to_vscode_settings_json(value: JsonValue) -> JsonValue { settings_json } -pub async fn run_setup() { - generate_typedef_files_from_definitions(&TYPEDEFS_DIR) - .await - .expect("Failed to generate typedef files"); - // TODO: Let the user interactively choose what editor to set up - let res = async { - let settings = read_or_create_vscode_settings_json().await?; - let modified = add_values_to_vscode_settings_json(settings); - write_vscode_settings_json(modified).await?; - Ok::<_, SetupError>(()) - } - .await; - let message = match res { - Ok(()) => "These settings have been added to your workspace for Visual Studio Code:", - Err(_) => "To finish setting up your editor, add these settings to your workspace:", - }; - let version_string = lune_version(); - println!( - "Lune has now been set up and editor type definitions have been generated.\ - \n{message}\ - \n\ - \n\"{SETTING_NAME_MODE}\": \"relativeToFile\",\ - \n\"{SETTING_NAME_ALIASES}\": {{\ - \n \"@lune/\": \"~/.lune/.typedefs/{version_string}/\"\ - \n}}", - ); +async fn generate_typedef_files_from_definitions(dir: &Dir<'_>) -> Result { + let contents = read_typedefs_dir_contents(dir); + write_typedef_files(contents).await +} + +fn read_typedefs_dir_contents(dir: &Dir<'_>) -> HashMap> { + let mut definitions = HashMap::new(); + + for entry in dir.find("*.luau").unwrap() { + let entry_file = entry.as_file().unwrap(); + let entry_name = entry_file.path().file_name().unwrap().to_string_lossy(); + + let typedef_name = entry_name.trim_end_matches(".luau"); + let typedef_contents = entry_file.contents().to_vec(); + + definitions.insert(typedef_name.to_string(), typedef_contents); + } + + definitions +} + +async fn write_typedef_files(typedef_files: HashMap>) -> Result { + let version_string = env!("CARGO_PKG_VERSION"); + let mut dirs_to_write = Vec::new(); + let mut files_to_write = Vec::new(); + // Create the typedefs dir in the users cache dir + let cache_dir = UserDirs::new() + .context("Failed to find user home directory")? + .home_dir() + .join(".lune") + .join(".typedefs") + .join(version_string); + dirs_to_write.push(cache_dir.clone()); + // Make typedef files + for (builtin_name, builtin_typedef) in typedef_files { + let path = cache_dir + .join(builtin_name.to_ascii_lowercase()) + .with_extension("luau"); + files_to_write.push((builtin_name.to_lowercase(), path, builtin_typedef)); + } + // Write all dirs and files only when we know generation was successful + let futs_dirs = dirs_to_write + .drain(..) + .map(fs::create_dir_all) + .collect::>(); + let futs_files = files_to_write + .iter() + .map(|(_, path, contents)| fs::write(path, contents)) + .collect::>(); + try_join_all(futs_dirs).await?; + try_join_all(futs_files).await?; + Ok(version_string.to_string()) } diff --git a/src/main.rs b/src/main.rs index 3cdb821..01c834a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,8 +10,6 @@ use std::process::ExitCode; -use clap::Parser; - pub(crate) mod cli; pub(crate) mod executor; @@ -36,7 +34,7 @@ async fn main() -> ExitCode { return executor::run_standalone(bin).await.unwrap(); } - match Cli::parse().run().await { + match Cli::new().run().await { Ok(code) => code, Err(err) => { eprintln!(