Skip to main content

lux_lib/operations/
gen_luaurc.rs

1use crate::{
2    config::Config,
3    fs,
4    lockfile::LockedPackageLockType,
5    lua_version::LuaVersion,
6    tree::InstallTree,
7    workspace::{Workspace, WorkspaceError, WorkspaceTreeError, LUX_DIR_NAME},
8};
9use bon::Builder;
10use miette::Diagnostic;
11use path_slash::PathBufExt;
12use pathdiff::diff_paths;
13use serde::{Deserialize, Serialize};
14use std::collections::BTreeMap;
15use thiserror::Error;
16
17const LUAURC: &str = ".luaurc";
18
19#[derive(Error, Debug, Diagnostic)]
20pub enum GenLuauRcError {
21    #[error(transparent)]
22    #[diagnostic(transparent)]
23    Fs(#[from] fs::FsError),
24    #[error(transparent)]
25    #[diagnostic(transparent)]
26    Workspace(#[from] WorkspaceError),
27    #[error(transparent)]
28    #[diagnostic(transparent)]
29    WorkspaceTree(#[from] WorkspaceTreeError),
30    #[error("failed to serialize .luaurc content:\n{0}")]
31    Serialize(String),
32    #[error("failed to deserialize .luaurc content:\n{0}")]
33    Deserialize(String),
34}
35
36#[derive(Builder)]
37#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
38pub(crate) struct GenLuauRc<'a> {
39    config: &'a Config,
40    workspace: &'a Workspace,
41}
42
43impl<State> GenLuauRcBuilder<'_, State>
44where
45    State: gen_luau_rc_builder::State + gen_luau_rc_builder::IsComplete,
46{
47    /// Generates a .luaurc in the workspace root.
48    /// No-op if the specified workspace tree is not a Luau tree.
49    pub async fn generate_luau_rc(self) -> Result<(), GenLuauRcError> {
50        do_generate_luau_rc(self._build()).await
51    }
52}
53
54#[derive(Serialize, Deserialize, Default, PartialEq, Debug)]
55#[serde(default)]
56struct LuauRc {
57    #[serde(flatten)] // <-- capture any unknown keys here
58    other: BTreeMap<String, serde_json::Value>,
59
60    #[serde(default)]
61    aliases: BTreeMap<String, String>,
62}
63
64async fn do_generate_luau_rc(args: GenLuauRc<'_>) -> Result<(), GenLuauRcError> {
65    let workspace = args.workspace;
66    let tree = workspace.tree(args.config)?;
67    if tree.version() != &LuaVersion::Luau {
68        return Ok(());
69    }
70    let lockfile = workspace.lockfile()?;
71    let luaurc_path = workspace.root().join(LUAURC);
72
73    let prev_content = fs::tokio::read_to_string(&luaurc_path)
74        .await
75        .unwrap_or_else(|_| "{}".into());
76
77    let test_tree = workspace.test_tree(args.config)?;
78    let mut aliases = BTreeMap::new();
79    for (lock_type, tree) in [
80        (LockedPackageLockType::Regular, Some(&tree)),
81        (LockedPackageLockType::Test, Some(&test_tree)),
82    ] {
83        let Some(tree) = tree else {
84            continue;
85        };
86        for package in lockfile.local_pkg_lock(&lock_type).rocks().values() {
87            let layout = tree.layout_for(package.spec());
88            if layout.src.is_dir() {
89                if let Some(rel_path) = diff_paths(&layout.src, workspace.root()) {
90                    // NOTE: luau only resolves alias targets that are explicitly
91                    // relative (`./`/`../`) or absolute
92                    aliases.insert(
93                        package.name().to_string(),
94                        format!("./{}", rel_path.to_slash_lossy()),
95                    );
96                };
97            }
98        }
99    }
100
101    let luaurc_content = update_luau_rc_content(&prev_content, aliases)?;
102
103    fs::tokio::write(&luaurc_path, luaurc_content).await?;
104
105    Ok(())
106}
107
108fn update_luau_rc_content(
109    prev_contents: &str,
110    aliases: BTreeMap<String, String>,
111) -> Result<String, GenLuauRcError> {
112    let mut luaurc: LuauRc = serde_json::from_str(prev_contents)
113        .map_err(|err| GenLuauRcError::Deserialize(err.to_string()))?;
114
115    // remove any preexisting lux-managed aliases
116    luaurc.aliases.retain(|_, path| {
117        !path
118            .strip_prefix("./")
119            .unwrap_or(path)
120            .starts_with(&format!("{LUX_DIR_NAME}/"))
121    });
122
123    luaurc.aliases.extend(aliases);
124
125    serde_json::to_string_pretty(&luaurc).map_err(|err| GenLuauRcError::Serialize(err.to_string()))
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_update_luau_rc_content() {
134        let cases = vec![
135            (
136                "empty previous content, adding single alias",
137                "{}",
138                BTreeMap::from([("my-lib".into(), "./.lux/luau/lib-1.0.0/my-lib/src".into())]),
139                r#"{
140                    "aliases": {
141                        "my-lib": "./.lux/luau/lib-1.0.0/my-lib/src"
142                    }
143                }"#,
144            ),
145            (
146                "other fields preserved, lux-managed aliases refreshed",
147                r#"{
148                    "language": { "type": "luau" },
149                    "aliases": {
150                        "old": ".lux/luau/old/src",
151                        "user-alias": "../shared"
152                    }
153                }"#,
154                BTreeMap::from([("new-lib".into(), "./.lux/luau/new/src".into())]),
155                r#"{
156                    "language": { "type": "luau" },
157                    "aliases": {
158                        "new-lib": "./.lux/luau/new/src",
159                        "user-alias": "../shared"
160                    }
161                }"#,
162            ),
163        ];
164
165        for (description, initial, new_aliases, expected) in cases {
166            let content = super::update_luau_rc_content(initial, new_aliases.clone()).unwrap();
167            assert_eq!(
168                serde_json::from_str::<LuauRc>(&content).unwrap(),
169                serde_json::from_str::<LuauRc>(expected).unwrap(),
170                "Case failed: {}\nInitial input:\n{}\nNew aliases: {:?}",
171                description,
172                initial,
173                new_aliases
174            );
175        }
176    }
177}