lux_lib/operations/
gen_luaurc.rs1use 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 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)] 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 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 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}