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!(