mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 04:50:36 +00:00
Refactor CLI to use subcommands, update changelog
This commit is contained in:
parent
3565c897c0
commit
d7c603e881
9 changed files with 415 additions and 372 deletions
34
CHANGELOG.md
34
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: <br/>
|
||||
|
||||
- `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. <br/>
|
||||
|
@ -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`.
|
||||
|
|
|
@ -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<Path>,
|
||||
output_path: impl AsRef<Path>,
|
||||
source_code: impl AsRef<[u8]>,
|
||||
) -> Result<ExitCode> {
|
||||
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<PathBuf>,
|
||||
}
|
||||
|
||||
// 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<ExitCode> {
|
||||
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<Path>, bytes: impl AsRef<[u8]>) -> Result<()> {
|
||||
|
|
|
@ -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<String> {
|
||||
let contents = read_typedefs_dir_contents(dir);
|
||||
write_typedef_files(contents).await
|
||||
}
|
||||
|
||||
fn read_typedefs_dir_contents(dir: &Dir<'_>) -> HashMap<String, Vec<u8>> {
|
||||
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<String, Vec<u8>>) -> Result<String> {
|
||||
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::<Vec<_>>();
|
||||
let futs_files = files_to_write
|
||||
.iter()
|
||||
.map(|(_, path, contents)| write(path, contents))
|
||||
.collect::<Vec<_>>();
|
||||
try_join_all(futs_dirs).await?;
|
||||
try_join_all(futs_files).await?;
|
||||
Ok(version_string.to_string())
|
||||
}
|
47
src/cli/list.rs
Normal file
47
src/cli/list.rs
Normal file
|
@ -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<ExitCode> {
|
||||
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)
|
||||
}
|
||||
}
|
196
src/cli/mod.rs
196
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<String>,
|
||||
/// Arguments to pass to the script, stored in process.args
|
||||
script_args: Vec<String>,
|
||||
/// 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<CliSubcommand>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Cli {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
Self::parse()
|
||||
}
|
||||
|
||||
pub fn with_path<S>(mut self, path: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.script_path = Some(path.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_args<A>(mut self, args: A) -> Self
|
||||
where
|
||||
A: Into<Vec<String>>,
|
||||
{
|
||||
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<ExitCode> {
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
169
src/cli/repl.rs
169
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<ExitCode> {
|
||||
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<ExitCode> {
|
||||
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)
|
||||
}
|
||||
|
|
56
src/cli/run.rs
Normal file
56
src/cli/run.rs
Normal file
|
@ -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<String>,
|
||||
}
|
||||
|
||||
impl RunCommand {
|
||||
pub async fn run(self) -> Result<ExitCode> {
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
}
|
129
src/cli/setup.rs
129
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<ExitCode> {
|
||||
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<String> {
|
||||
let contents = read_typedefs_dir_contents(dir);
|
||||
write_typedef_files(contents).await
|
||||
}
|
||||
|
||||
fn read_typedefs_dir_contents(dir: &Dir<'_>) -> HashMap<String, Vec<u8>> {
|
||||
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<String, Vec<u8>>) -> Result<String> {
|
||||
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::<Vec<_>>();
|
||||
let futs_files = files_to_write
|
||||
.iter()
|
||||
.map(|(_, path, contents)| fs::write(path, contents))
|
||||
.collect::<Vec<_>>();
|
||||
try_join_all(futs_dirs).await?;
|
||||
try_join_all(futs_files).await?;
|
||||
Ok(version_string.to_string())
|
||||
}
|
||||
|
|
|
@ -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!(
|
||||
|
|
Loading…
Reference in a new issue