mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 04:50:36 +00:00
Rewrite setup subcommand to be more permissive and user friendly
This commit is contained in:
parent
65f2319a64
commit
66e122ea63
15 changed files with 149 additions and 254 deletions
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
|
@ -3,14 +3,8 @@
|
|||
"luau-lsp.sourcemap.enabled": false,
|
||||
"luau-lsp.types.roblox": false,
|
||||
"luau-lsp.require.mode": "relativeToFile",
|
||||
"luau-lsp.require.fileAliases": {
|
||||
"@lune/fs": "./docs/typedefs/FS.luau",
|
||||
"@lune/net": "./docs/typedefs/Net.luau",
|
||||
"@lune/process": "./docs/typedefs/Process.luau",
|
||||
"@lune/roblox": "./docs/typedefs/Roblox.luau",
|
||||
"@lune/serde": "./docs/typedefs/Serde.luau",
|
||||
"@lune/stdio": "./docs/typedefs/Stdio.luau",
|
||||
"@lune/task": "./docs/typedefs/Task.luau"
|
||||
"luau-lsp.require.directoryAliases": {
|
||||
"@lune/": "./docs/typedefs"
|
||||
},
|
||||
// Luau - ignore type defs file in docs dir and dev scripts we use
|
||||
"luau-lsp.ignoreGlobs": [
|
||||
|
|
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1135,6 +1135,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ console = "0.15"
|
|||
directories = "5.0"
|
||||
futures-util = "0.3"
|
||||
once_cell = "1.17"
|
||||
thiserror = "1.0"
|
||||
|
||||
mlua = { version = "0.9.0-beta.3", features = ["luau", "serialize"] }
|
||||
tokio = { version = "1.24", features = ["full"] }
|
||||
|
|
|
@ -31,6 +31,7 @@ serde.workspace = true
|
|||
serde_json.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
tokio.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
anyhow = "1.0"
|
||||
env_logger = "0.10"
|
||||
|
|
|
@ -1,32 +1,22 @@
|
|||
use std::{
|
||||
borrow::BorrowMut,
|
||||
collections::HashMap,
|
||||
fmt::Write as _,
|
||||
path::{Path, PathBuf},
|
||||
process::ExitCode,
|
||||
};
|
||||
use std::{fmt::Write as _, process::ExitCode};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{CommandFactory, Parser};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use include_dir::{include_dir, Dir};
|
||||
use lune::Lune;
|
||||
use tokio::{
|
||||
fs::{self, read as read_to_vec},
|
||||
fs::read as read_to_vec,
|
||||
io::{stdin, AsyncReadExt},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
gen::{generate_gitbook_dir_from_definitions, generate_typedef_files_from_definitions},
|
||||
setup::run_setup,
|
||||
utils::{
|
||||
files::{discover_script_file_path_including_lune_dirs, strip_shebang},
|
||||
listing::{find_lune_scripts, sort_lune_scripts, write_lune_scripts_list},
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) static TYPEDEFS_DIR: Dir<'_> = include_dir!("docs/typedefs");
|
||||
|
||||
/// A Luau script runner
|
||||
#[derive(Parser, Debug, Default, Clone)]
|
||||
#[command(version, long_about = None)]
|
||||
|
@ -51,9 +41,6 @@ pub struct Cli {
|
|||
/// Generate a Lune documentation file for Luau LSP
|
||||
#[clap(long, hide = true)]
|
||||
generate_docs_file: bool,
|
||||
/// Generate the full Lune gitbook directory
|
||||
#[clap(long, hide = true)]
|
||||
generate_gitbook_dir: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
@ -134,61 +121,21 @@ impl Cli {
|
|||
let generate_file_requested = self.setup
|
||||
|| self.generate_luau_types
|
||||
|| self.generate_selene_types
|
||||
|| self.generate_docs_file
|
||||
|| self.generate_gitbook_dir;
|
||||
|| self.generate_docs_file;
|
||||
if generate_file_requested {
|
||||
if self.generate_gitbook_dir {
|
||||
generate_gitbook_dir_from_definitions(&TYPEDEFS_DIR).await?;
|
||||
}
|
||||
if (self.generate_luau_types || self.generate_selene_types || self.generate_docs_file)
|
||||
&& !self.setup
|
||||
{
|
||||
eprintln!(
|
||||
"\
|
||||
Typedef & docs generation files have been superseded by the --setup command.\
|
||||
Run lune --setup in your terminal to configure typedef files.
|
||||
Typedef & docs generation commands have been superseded by the setup command.\
|
||||
Run `lune --setup` in your terminal to configure your editor and type definitions.
|
||||
"
|
||||
);
|
||||
return Ok(ExitCode::FAILURE);
|
||||
}
|
||||
if self.setup {
|
||||
let generated_paths =
|
||||
generate_typedef_files_from_definitions(&TYPEDEFS_DIR).await?;
|
||||
let settings_json_path = PathBuf::from(".vscode/settings.json");
|
||||
let message = match fs::metadata(&settings_json_path).await {
|
||||
Ok(meta) if meta.is_file() => {
|
||||
if try_add_generated_typedefs_vscode(&settings_json_path, &generated_paths).await.is_err() {
|
||||
"These files can be added to your LSP settings for autocomplete and documentation."
|
||||
} else {
|
||||
"These files have now been added to your workspace LSP settings for Visual Studio Code."
|
||||
}
|
||||
}
|
||||
_ => "These files can be added to your LSP settings for autocomplete and documentation.",
|
||||
};
|
||||
// HACK: We should probably just be serializing this hashmap to print it out, but
|
||||
// that does not guarantee sorting and the sorted version is much easier to read
|
||||
let mut sorted_names = generated_paths
|
||||
.keys()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
sorted_names.sort_unstable();
|
||||
println!(
|
||||
"Typedefs have been generated in the following locations:\n{{\n{}\n}}\n{message}",
|
||||
sorted_names
|
||||
.iter()
|
||||
.map(|name| {
|
||||
let path = generated_paths.get(name).unwrap();
|
||||
format!(
|
||||
" \"@lune/{}\": \"{}\",",
|
||||
name,
|
||||
path.canonicalize().unwrap().display()
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
.strip_suffix(',')
|
||||
.unwrap()
|
||||
);
|
||||
run_setup().await;
|
||||
}
|
||||
}
|
||||
if self.script_path.is_none() {
|
||||
|
@ -236,29 +183,3 @@ impl Cli {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_add_generated_typedefs_vscode(
|
||||
settings_json_path: &Path,
|
||||
generated_paths: &HashMap<String, PathBuf>,
|
||||
) -> Result<()> {
|
||||
// FUTURE: Use a jsonc or json5 to read this file instead since it may contain comments and fail
|
||||
let settings_json_contents = fs::read(settings_json_path).await?;
|
||||
let mut settings_changed: bool = false;
|
||||
let mut settings_json: JsonValue = serde_json::from_slice(&settings_json_contents)?;
|
||||
if let JsonValue::Object(settings) = settings_json.borrow_mut() {
|
||||
if let Some(JsonValue::Object(aliases)) = settings.get_mut("luau-lsp.require.fileAliases") {
|
||||
for (name, path) in generated_paths {
|
||||
settings_changed = true;
|
||||
aliases.insert(
|
||||
format!("@lune/{name}"),
|
||||
JsonValue::String(path.canonicalize().unwrap().to_string_lossy().to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if settings_changed {
|
||||
let settings_json_new = serde_json::to_vec_pretty(&settings_json)?;
|
||||
fs::write(settings_json_path, settings_json_new).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use include_dir::Dir;
|
||||
|
@ -15,9 +15,7 @@ pub async fn generate_gitbook_dir_from_definitions(dir: &Dir<'_>) -> Result<()>
|
|||
gitbook_dir::generate_from_type_definitions(definitions).await
|
||||
}
|
||||
|
||||
pub async fn generate_typedef_files_from_definitions(
|
||||
dir: &Dir<'_>,
|
||||
) -> Result<HashMap<String, PathBuf>> {
|
||||
pub async fn generate_typedef_files_from_definitions(dir: &Dir<'_>) -> Result<String> {
|
||||
let contents = read_typedefs_dir_contents(dir);
|
||||
typedef_files::generate_from_type_definitions(contents).await
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use directories::UserDirs;
|
||||
|
@ -9,7 +9,8 @@ use tokio::fs::{create_dir_all, write};
|
|||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn generate_from_type_definitions(
|
||||
typedef_files: HashMap<String, Vec<u8>>,
|
||||
) -> Result<HashMap<String, PathBuf>> {
|
||||
) -> 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
|
||||
|
@ -18,7 +19,7 @@ pub async fn generate_from_type_definitions(
|
|||
.home_dir()
|
||||
.join(".lune")
|
||||
.join(".typedefs")
|
||||
.join(env!("CARGO_PKG_VERSION"));
|
||||
.join(version_string);
|
||||
dirs_to_write.push(cache_dir.clone());
|
||||
// Make typedef files
|
||||
for (builtin_name, builtin_typedef) in typedef_files {
|
||||
|
@ -38,8 +39,5 @@ pub async fn generate_from_type_definitions(
|
|||
.collect::<Vec<_>>();
|
||||
try_join_all(futs_dirs).await?;
|
||||
try_join_all(futs_files).await?;
|
||||
Ok(files_to_write
|
||||
.drain(..)
|
||||
.map(|(name, path, _)| (name, path))
|
||||
.collect::<HashMap<_, _>>())
|
||||
Ok(version_string.to_string())
|
||||
}
|
||||
|
|
|
@ -14,11 +14,9 @@ use clap::Parser;
|
|||
|
||||
pub(crate) mod cli;
|
||||
pub(crate) mod gen;
|
||||
pub(crate) mod setup;
|
||||
pub(crate) mod utils;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use cli::Cli;
|
||||
use console::style;
|
||||
|
||||
|
|
128
packages/cli/src/setup/mod.rs
Normal file
128
packages/cli/src/setup/mod.rs
Normal file
|
@ -0,0 +1,128 @@
|
|||
use std::{borrow::BorrowMut, env::current_dir, io::ErrorKind, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use include_dir::{include_dir, Dir};
|
||||
use thiserror::Error;
|
||||
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 crate::gen::generate_typedef_files_from_definitions;
|
||||
|
||||
pub(crate) static TYPEDEFS_DIR: Dir<'_> = include_dir!("docs/typedefs");
|
||||
|
||||
pub(crate) static SETTING_NAME_MODE: &str = "luau-lsp.require.mode";
|
||||
pub(crate) static SETTING_NAME_ALIASES: &str = "luau-lsp.require.directoryAliases";
|
||||
|
||||
#[derive(Debug, Clone, Copy, Error)]
|
||||
enum SetupError {
|
||||
#[error("Failed to read settings")]
|
||||
Read,
|
||||
#[error("Failed to write settings")]
|
||||
Write,
|
||||
#[error("Failed to parse settings")]
|
||||
Deserialize,
|
||||
#[error("Failed to create settings")]
|
||||
Serialize,
|
||||
}
|
||||
|
||||
fn lune_version() -> &'static str {
|
||||
env!("CARGO_PKG_VERSION")
|
||||
}
|
||||
|
||||
fn vscode_path() -> PathBuf {
|
||||
current_dir()
|
||||
.expect("No current dir")
|
||||
.join(".vscode")
|
||||
.join("settings.json")
|
||||
}
|
||||
|
||||
async fn read_or_create_vscode_settings_json() -> Result<JsonValue, SetupError> {
|
||||
let path_file = vscode_path();
|
||||
let mut path_dir = path_file.clone();
|
||||
path_dir.pop();
|
||||
match fs::read(&path_file).await {
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => {
|
||||
// TODO: Make sure that VSCode is actually installed, or
|
||||
// let the user choose their editor for interactive setup
|
||||
match fs::create_dir_all(path_dir).await {
|
||||
Err(_) => Err(SetupError::Write),
|
||||
Ok(_) => match fs::write(path_file, "{}").await {
|
||||
Err(_) => Err(SetupError::Write),
|
||||
Ok(_) => Ok(JsonValue::Object(serde_json::Map::new())),
|
||||
},
|
||||
}
|
||||
}
|
||||
Err(_) => Err(SetupError::Read),
|
||||
Ok(contents) => match serde_json::from_slice(&contents) {
|
||||
Err(_) => Err(SetupError::Deserialize),
|
||||
Ok(json) => Ok(json),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_vscode_settings_json(value: JsonValue) -> Result<(), SetupError> {
|
||||
match serde_json::to_vec_pretty(&value) {
|
||||
Err(_) => Err(SetupError::Serialize),
|
||||
Ok(json) => match fs::write(vscode_path(), json).await {
|
||||
Err(_) => Err(SetupError::Write),
|
||||
Ok(_) => Ok(()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn add_values_to_vscode_settings_json(value: JsonValue) -> JsonValue {
|
||||
let mut settings_json = value;
|
||||
if let JsonValue::Object(settings) = settings_json.borrow_mut() {
|
||||
// Set require mode
|
||||
let mode_val = "relativeToFile".to_string();
|
||||
settings.insert(SETTING_NAME_MODE.to_string(), JsonValue::String(mode_val));
|
||||
// Set require alias to our typedefs
|
||||
let aliases_key = "@lune/".to_string();
|
||||
let aliases_val = format!("~/.lune/.typedefs/{}/", lune_version());
|
||||
if let Some(JsonValue::Object(aliases)) = settings.get_mut(SETTING_NAME_ALIASES) {
|
||||
if aliases.contains_key(&aliases_key) {
|
||||
if aliases.get(&aliases_key).unwrap() != &JsonValue::String(aliases_val.to_string())
|
||||
{
|
||||
aliases.insert(aliases_key, JsonValue::String(aliases_val));
|
||||
}
|
||||
} else {
|
||||
aliases.insert(aliases_key, JsonValue::String(aliases_val));
|
||||
}
|
||||
} else {
|
||||
let mut map = serde_json::Map::new();
|
||||
map.insert(aliases_key, JsonValue::String(aliases_val));
|
||||
settings.insert(SETTING_NAME_ALIASES.to_string(), JsonValue::Object(map));
|
||||
}
|
||||
}
|
||||
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}}",
|
||||
);
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
use std::{env::set_current_dir, path::PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use tokio::fs::create_dir_all;
|
||||
|
||||
pub async fn enter_bin_dir() -> Result<()> {
|
||||
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../bin");
|
||||
if !path.exists() {
|
||||
create_dir_all(&path)
|
||||
.await
|
||||
.context("Failed to enter bin dir")?;
|
||||
set_current_dir(&path).context("Failed to set current dir")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn leave_bin_dir() -> Result<()> {
|
||||
set_current_dir(env!("CARGO_MANIFEST_DIR")).context("Failed to leave bin dir")?;
|
||||
Ok(())
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use tokio::fs::{read_to_string, remove_file};
|
||||
|
||||
use super::bin_dir::{enter_bin_dir, leave_bin_dir};
|
||||
use super::file_type::FileType;
|
||||
|
||||
pub fn fmt_path_relative_to_workspace_root(value: &str) -> String {
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../")
|
||||
.canonicalize()
|
||||
.unwrap();
|
||||
match PathBuf::from(value).strip_prefix(root) {
|
||||
Err(_) => format!("{:#?}", PathBuf::from(value).display()),
|
||||
Ok(inner) => format!("{:#?}", inner.display()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn inner(file_name: &str, desired_type: FileType) -> Result<()> {
|
||||
match read_to_string(file_name).await.with_context(|| {
|
||||
format!(
|
||||
"Failed to read definitions file at '{}'",
|
||||
fmt_path_relative_to_workspace_root(file_name)
|
||||
)
|
||||
}) {
|
||||
Ok(file_contents) => {
|
||||
remove_file(file_name).await.with_context(|| {
|
||||
format!(
|
||||
"Failed to remove definitions file at '{}'",
|
||||
fmt_path_relative_to_workspace_root(file_name)
|
||||
)
|
||||
})?;
|
||||
let parsed_type = FileType::sniff(&file_contents);
|
||||
if parsed_type != Some(desired_type) {
|
||||
bail!(
|
||||
"Generating definitions file at '{}' created '{}', expected '{}'",
|
||||
fmt_path_relative_to_workspace_root(file_name),
|
||||
parsed_type.map_or("unknown", |t| t.name()),
|
||||
desired_type.name()
|
||||
)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => bail!(
|
||||
"Failed to generate definitions file at '{}'\n{e}",
|
||||
fmt_path_relative_to_workspace_root(file_name)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn ensure_file_exists_and_is(file_name: &str, desired_type: FileType) -> Result<()> {
|
||||
enter_bin_dir().await?;
|
||||
let res = inner(file_name, desired_type).await;
|
||||
leave_bin_dir()?;
|
||||
res
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
use serde_json::Value as JsonValue;
|
||||
use serde_yaml::Value as YamlValue;
|
||||
|
||||
use crate::gen::definitions::DefinitionsTree;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FileType {
|
||||
Json,
|
||||
Yaml,
|
||||
Luau,
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
pub fn sniff(contents: &str) -> Option<Self> {
|
||||
if serde_json::from_str::<JsonValue>(contents).is_ok() {
|
||||
Some(Self::Json)
|
||||
} else if serde_yaml::from_str::<YamlValue>(contents).is_ok() {
|
||||
Some(Self::Yaml)
|
||||
} else if DefinitionsTree::from_type_definitions(contents).is_ok() {
|
||||
Some(Self::Luau)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
FileType::Json => "json",
|
||||
FileType::Yaml => "yaml",
|
||||
FileType::Luau => "luau",
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
use anyhow::Result;
|
||||
|
||||
use crate::cli::Cli;
|
||||
|
||||
mod bin_dir;
|
||||
mod file_checks;
|
||||
mod file_type;
|
||||
mod run_cli;
|
||||
|
||||
pub(crate) use run_cli::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn list() -> Result<()> {
|
||||
Cli::new().list().run().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn generate_typedef_files() -> Result<()> {
|
||||
run_cli(Cli::new().setup()).await?;
|
||||
// TODO: Implement test
|
||||
Ok(())
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
use anyhow::Result;
|
||||
|
||||
use crate::cli::Cli;
|
||||
|
||||
use super::bin_dir::{enter_bin_dir, leave_bin_dir};
|
||||
|
||||
pub async fn run_cli(cli: Cli) -> Result<()> {
|
||||
enter_bin_dir().await?;
|
||||
cli.run().await?;
|
||||
leave_bin_dir()?;
|
||||
Ok(())
|
||||
}
|
|
@ -17,10 +17,10 @@ path = "src/lib.rs"
|
|||
[dependencies]
|
||||
mlua.workspace = true
|
||||
once_cell.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
glam = "0.24"
|
||||
rand = "0.8"
|
||||
thiserror = "1.0"
|
||||
|
||||
rbx_binary = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "2e78feb05e033cbca8db1d9e490f8334c096d13e" }
|
||||
rbx_dom_weak = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "2e78feb05e033cbca8db1d9e490f8334c096d13e" }
|
||||
|
|
Loading…
Reference in a new issue