cluaupp 1.5.2 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/README.md +27 -40
  3. package/api/generated/bench-latest.json +43 -21
  4. package/api/generated/lsp-index.json +1 -1
  5. package/api/manifests/roblox-target.manifest.json +5 -5
  6. package/api/normalized/roblox-target.profile.json +8729 -267
  7. package/api/roblox/luau-builtins.d.luau +185 -0
  8. package/api/roblox/surface.json +1 -0
  9. package/api/roblox-api.lock.json +11 -2
  10. package/bin/cluaupp.js +38 -0
  11. package/editors/vscode/syntaxes/clpp.tmLanguage.json +211 -0
  12. package/examples/README.md +9 -9
  13. package/examples/game/cluaupp.config.json +2 -2
  14. package/examples/game/default.project.json +4 -15
  15. package/examples/game/out/ReplicatedStorage/Shared/GameInfo.luau +8 -0
  16. package/examples/game/out/ReplicatedStorage/Shared/GameInfo.luau.map.json +13 -0
  17. package/examples/game/out/ServerScriptService/Leaderstats.server.luau +26 -0
  18. package/examples/game/out/ServerScriptService/Leaderstats.server.luau.map.json +13 -0
  19. package/examples/game/out/ServerScriptService/Main.server.luau +15 -0
  20. package/examples/game/out/ServerScriptService/Main.server.luau.map.json +13 -0
  21. package/examples/game/out/StarterPlayer/StarterPlayerScripts/Main.client.luau +10 -0
  22. package/examples/game/out/StarterPlayer/StarterPlayerScripts/Main.client.luau.map.json +13 -0
  23. package/examples/game/src/ReplicatedStorage/Shared/GameInfo.clp +1 -0
  24. package/examples/game/src/ServerScriptService/Leaderstats.server.clp +18 -0
  25. package/examples/game/src/ServerScriptService/Main.server.clp +9 -0
  26. package/examples/game/src/StarterPlayer/StarterPlayerScripts/Main.client.clp +4 -0
  27. package/generated/api/build-profile.js +1 -1
  28. package/generated/api/loader.js +2 -2
  29. package/generated/api/surface/build.js +328 -0
  30. package/generated/api/surface/luau-defs.js +595 -0
  31. package/generated/api/surface/model.js +8 -0
  32. package/generated/clpp/contract.js +6 -3
  33. package/generated/clpp/handshake.js +36 -0
  34. package/generated/clpp/modules.js +20 -9
  35. package/generated/clpp/runner.js +62 -31
  36. package/generated/compile.js +18 -1
  37. package/generated/compiler/ast.js +3 -0
  38. package/generated/compiler/checker/calls.js +999 -0
  39. package/generated/compiler/checker/checker.js +1215 -0
  40. package/generated/compiler/checker/exprs.js +791 -0
  41. package/generated/compiler/checker/flow.js +168 -0
  42. package/generated/compiler/checker/members.js +763 -0
  43. package/generated/compiler/checker/statements.js +792 -0
  44. package/generated/compiler/checker/symbols.js +90 -0
  45. package/generated/compiler/checker/typeops.js +348 -0
  46. package/generated/compiler/checker/yields.js +115 -0
  47. package/generated/compiler/diagnostics.js +138 -0
  48. package/generated/compiler/emit/emitter.js +2356 -0
  49. package/generated/compiler/emit/luau-types.js +137 -0
  50. package/generated/compiler/emit/names.js +130 -0
  51. package/generated/compiler/emit/runtime.js +432 -0
  52. package/generated/compiler/lexer.js +403 -0
  53. package/generated/compiler/parser.js +1881 -0
  54. package/generated/compiler/project.js +332 -0
  55. package/generated/compiler/roblox/api.js +388 -0
  56. package/generated/compiler/roblox/context.js +100 -0
  57. package/generated/compiler/source.js +59 -0
  58. package/generated/compiler/stdlib.js +232 -0
  59. package/generated/compiler/types.js +250 -0
  60. package/generated/target/build-cache.js +4 -4
  61. package/generated/target/gen-rojo-tree.js +15 -0
  62. package/generated/target/source-map.js +61 -8
  63. package/generated/target/studio-bridge.js +1 -0
  64. package/generated/utils/project.js +83 -25
  65. package/native/bin/.gitkeep +1 -0
  66. package/package.json +17 -6
  67. package/plugins/CluauppNav/Plugin.luau +28 -4
  68. package/rust/cluaupp/Cargo.lock +795 -0
  69. package/rust/cluaupp/Cargo.toml +17 -0
  70. package/rust/cluaupp/src/main.rs +717 -0
  71. package/scripts/postinstall-native.js +56 -0
  72. package/skills/README.md +3 -3
  73. package/skills/clpp-hive-flare/SKILL.md +30 -26
  74. package/skills/clpp-language/SKILL.md +67 -74
  75. package/skills/clpp-language/compiler.md +24 -36
  76. package/skills/clpp-style/SKILL.md +31 -44
  77. package/src/api/build-profile.ts +1 -1
  78. package/src/api/loader.ts +2 -2
  79. package/src/api/surface/build.ts +374 -0
  80. package/src/api/surface/luau-defs.ts +635 -0
  81. package/src/api/surface/model.ts +96 -0
  82. package/src/clpp/contract.ts +20 -3
  83. package/src/clpp/handshake.ts +25 -0
  84. package/src/clpp/modules.ts +17 -10
  85. package/src/clpp/runner.ts +52 -34
  86. package/src/compile.ts +20 -2
  87. package/src/compiler/ast.ts +336 -0
  88. package/src/compiler/checker/calls.ts +1025 -0
  89. package/src/compiler/checker/checker.ts +1352 -0
  90. package/src/compiler/checker/exprs.ts +805 -0
  91. package/src/compiler/checker/flow.ts +174 -0
  92. package/src/compiler/checker/members.ts +793 -0
  93. package/src/compiler/checker/statements.ts +802 -0
  94. package/src/compiler/checker/symbols.ts +219 -0
  95. package/src/compiler/checker/typeops.ts +355 -0
  96. package/src/compiler/checker/yields.ts +120 -0
  97. package/src/compiler/diagnostics.ts +163 -0
  98. package/src/compiler/emit/emitter.ts +2486 -0
  99. package/src/compiler/emit/luau-types.ts +143 -0
  100. package/src/compiler/emit/names.ts +130 -0
  101. package/src/compiler/emit/runtime.ts +438 -0
  102. package/src/compiler/lexer.ts +444 -0
  103. package/src/compiler/parser.ts +1943 -0
  104. package/src/compiler/project.ts +354 -0
  105. package/src/compiler/roblox/api.ts +408 -0
  106. package/src/compiler/roblox/context.ts +108 -0
  107. package/src/compiler/source.ts +67 -0
  108. package/src/compiler/stdlib.ts +256 -0
  109. package/src/compiler/types.ts +313 -0
  110. package/src/target/build-cache.ts +5 -8
  111. package/src/target/gen-rojo-tree.ts +15 -0
  112. package/src/target/source-map.ts +65 -27
  113. package/src/target/studio-bridge.ts +3 -0
  114. package/src/types.ts +3 -0
  115. package/src/utils/project.ts +79 -31
  116. package/templates/README.md +7 -0
  117. package/templates/{game → game-modern}/cluaupp.config.json +2 -2
  118. package/templates/{game → game-modern}/package.json +1 -2
  119. package/templates/game-modern/rokit.toml +2 -0
  120. package/templates/game-modern/src/ReplicatedStorage/Shared/GameInfo.clp +1 -0
  121. package/templates/game-modern/src/ServerScriptService/Handlers/Guard.server.clp +4 -0
  122. package/templates/game-modern/src/ServerScriptService/Main.server.clp +8 -0
  123. package/templates/game-modern/src/StarterPlayer/StarterPlayerScripts/Main.client.clp +4 -0
  124. package/examples/game/out/ReplicatedStorage/Shared/Constants/Datas/TemplateData.luau +0 -29
  125. package/examples/game/out/ReplicatedStorage/Shared/Constants/Datas/TemplateData.luau.map.json +0 -13
  126. package/examples/game/out/ReplicatedStorage/Shared/Net/Net.luau +0 -83
  127. package/examples/game/out/ReplicatedStorage/Shared/Net/Net.luau.map.json +0 -13
  128. package/examples/game/out/ServerScriptService/Boot/DataBoot.server.luau +0 -18
  129. package/examples/game/out/ServerScriptService/Boot/DataBoot.server.luau.map.json +0 -13
  130. package/examples/game/out/ServerScriptService/Configuration/ServerConfig.luau +0 -14
  131. package/examples/game/out/ServerScriptService/Configuration/ServerConfig.luau.map.json +0 -13
  132. package/examples/game/out/ServerScriptService/Handlers/Guard.server.luau +0 -15
  133. package/examples/game/out/ServerScriptService/Handlers/Guard.server.luau.map.json +0 -13
  134. package/examples/game/out/ServerScriptService/Handlers/PlayerHandler.server.luau +0 -50
  135. package/examples/game/out/ServerScriptService/Handlers/PlayerHandler.server.luau.map.json +0 -13
  136. package/examples/game/out/StarterPlayer/StarterCharacterScripts/Character/CharacterReady.client.luau +0 -12
  137. package/examples/game/out/StarterPlayer/StarterCharacterScripts/Character/CharacterReady.client.luau.map.json +0 -13
  138. package/examples/game/out/StarterPlayer/StarterPlayerScripts/Controllers/DataController.client.luau +0 -16
  139. package/examples/game/out/StarterPlayer/StarterPlayerScripts/Controllers/DataController.client.luau.map.json +0 -13
  140. package/examples/game/src/Client/Controllers/DataController.client.clpp +0 -7
  141. package/examples/game/src/Client/Main.client.clpp +0 -4
  142. package/examples/game/src/Declarations/Net.flare +0 -5
  143. package/examples/game/src/Include/GameTypes.clh +0 -40
  144. package/examples/game/src/Modules/Core/Guard.server.clpp +0 -7
  145. package/examples/game/src/Modules/Core/PlayerHandler.server.clpp +0 -23
  146. package/examples/game/src/Server/Boot/DataBoot.server.clpp +0 -13
  147. package/examples/game/src/Server/Main.server.clpp +0 -4
  148. package/templates/game/.clangd +0 -25
  149. package/templates/game/.vscode/extensions.json +0 -8
  150. package/templates/game/.vscode/settings.json +0 -17
  151. package/templates/game/Packages/.gitkeep +0 -0
  152. package/templates/game/compile_flags.txt +0 -6
  153. package/templates/game/default.project.json +0 -41
  154. package/templates/game/include/clpp/datatypes.clh +0 -575
  155. package/templates/game/include/clpp/generated/enums.clh +0 -4920
  156. package/templates/game/include/clpp/generated/instances.clh +0 -87182
  157. package/templates/game/include/clpp/libs/arrayindexer.clh +0 -12
  158. package/templates/game/include/clpp/libs/axiom.clh +0 -154
  159. package/templates/game/include/clpp/libs/bloom.clh +0 -58
  160. package/templates/game/include/clpp/libs/coil.clh +0 -37
  161. package/templates/game/include/clpp/libs/crest.clh +0 -22
  162. package/templates/game/include/clpp/libs/echo.clh +0 -52
  163. package/templates/game/include/clpp/libs/ember.clh +0 -17
  164. package/templates/game/include/clpp/libs/flare.clh +0 -65
  165. package/templates/game/include/clpp/libs/gleam.clh +0 -111
  166. package/templates/game/include/clpp/libs/guide.clh +0 -42
  167. package/templates/game/include/clpp/libs/helm.clh +0 -31
  168. package/templates/game/include/clpp/libs/hive.clh +0 -20
  169. package/templates/game/include/clpp/libs/keep.clh +0 -158
  170. package/templates/game/include/clpp/libs/lens.clh +0 -24
  171. package/templates/game/include/clpp/libs/mint.clh +0 -28
  172. package/templates/game/include/clpp/libs/net.clh +0 -31
  173. package/templates/game/include/clpp/libs/occlude.clh +0 -15
  174. package/templates/game/include/clpp/libs/pin.clh +0 -28
  175. package/templates/game/include/clpp/libs/promise.clh +0 -24
  176. package/templates/game/include/clpp/libs/roster.clh +0 -123
  177. package/templates/game/include/clpp/libs/shift.clh +0 -22
  178. package/templates/game/include/clpp/libs/spark.clh +0 -28
  179. package/templates/game/include/clpp/libs/stage.clh +0 -23
  180. package/templates/game/include/clpp/libs/sweep.clh +0 -23
  181. package/templates/game/include/clpp/libs/trace.clh +0 -18
  182. package/templates/game/include/clpp/libs/ward.clh +0 -19
  183. package/templates/game/include/clpp/libs.clh +0 -31
  184. package/templates/game/include/clpp/roblox.clh +0 -41
  185. package/templates/game/rokit.toml +0 -5
  186. package/templates/game/src/Client/Controllers/DataController.client.clpp +0 -7
  187. package/templates/game/src/Client/Main.client.clpp +0 -4
  188. package/templates/game/src/Declarations/Net.flare +0 -5
  189. package/templates/game/src/Include/GameTypes.clh +0 -40
  190. package/templates/game/src/Modules/Core/Guard.server.clpp +0 -7
  191. package/templates/game/src/Modules/Core/PlayerHandler.server.clpp +0 -24
  192. package/templates/game/src/Server/Boot/DataBoot.server.clpp +0 -13
  193. package/templates/game/src/Server/Main.server.clpp +0 -4
  194. package/templates/game/stylua.toml +0 -9
  195. package/templates/game/wally.toml +0 -12
@@ -0,0 +1,717 @@
1
+ use anyhow::{anyhow, Context, Result};
2
+ use clap::{Parser, Subcommand};
3
+ use notify::{Config as NotifyConfig, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
4
+ use regex::Regex;
5
+ use serde::{Deserialize, Serialize};
6
+ use serde_json::json;
7
+ use sha2::{Digest, Sha256};
8
+ use std::collections::HashSet;
9
+ use std::ffi::OsStr;
10
+ use std::fs;
11
+ use std::path::{Path, PathBuf};
12
+ use std::process::Stdio;
13
+ use std::time::Duration;
14
+ use tokio::io::AsyncWriteExt;
15
+ use tokio::process::Command;
16
+ use tokio::sync::Semaphore;
17
+ use walkdir::WalkDir;
18
+
19
+ #[derive(Parser)]
20
+ #[command(name = "cluaupp", about = "Cluaupp native orchestrator CLI")]
21
+ struct Cli {
22
+ #[command(subcommand)]
23
+ command: Commands,
24
+ }
25
+
26
+ #[derive(Subcommand)]
27
+ enum Commands {
28
+ Init {
29
+ #[arg(default_value = ".")]
30
+ folder: String,
31
+ },
32
+ Build {
33
+ #[arg(default_value = ".")]
34
+ folder: String,
35
+ #[arg(long, default_value = "default.project.json")]
36
+ rojo: String,
37
+ #[arg(long, default_value_t = true)]
38
+ incremental: bool,
39
+ #[arg(short = 'j', long)]
40
+ jobs: Option<usize>,
41
+ },
42
+ Watch {
43
+ #[arg(default_value = ".")]
44
+ folder: String,
45
+ #[arg(long, default_value = "default.project.json")]
46
+ rojo: String,
47
+ #[arg(short = 'j', long)]
48
+ jobs: Option<usize>,
49
+ },
50
+ Install {
51
+ #[arg(default_value = ".")]
52
+ _folder: String,
53
+ },
54
+ UpdateTypes {
55
+ #[arg(default_value = ".")]
56
+ folder: String,
57
+ },
58
+ }
59
+
60
+ #[derive(Debug, Clone, Serialize, Deserialize)]
61
+ struct ProjectConfig {
62
+ #[serde(default = "default_root_dir", rename = "rootDir")]
63
+ root_dir: String,
64
+ #[serde(default = "default_out_dir", rename = "outDir")]
65
+ out_dir: String,
66
+ #[serde(default)]
67
+ strict: bool,
68
+ }
69
+
70
+ #[derive(Debug, Deserialize)]
71
+ struct ManifestToml {
72
+ compiler: Option<ManifestCompiler>,
73
+ dependencies: Option<HashMapToml>,
74
+ }
75
+
76
+ #[derive(Debug, Deserialize)]
77
+ struct ManifestCompiler {
78
+ #[serde(default, rename = "root_dir")]
79
+ root_dir: Option<String>,
80
+ #[serde(default, rename = "out_dir")]
81
+ out_dir: Option<String>,
82
+ #[serde(default)]
83
+ strict: Option<bool>,
84
+ }
85
+
86
+ type HashMapToml = std::collections::BTreeMap<String, toml::Value>;
87
+
88
+ fn default_root_dir() -> String {
89
+ "Src".to_string()
90
+ }
91
+
92
+ fn default_out_dir() -> String {
93
+ "out".to_string()
94
+ }
95
+
96
+ #[derive(Debug, Deserialize)]
97
+ struct CompileArtifact {
98
+ ok: bool,
99
+ luau: String,
100
+ #[serde(default, rename = "fileName")]
101
+ file_name: String,
102
+ #[serde(default)]
103
+ error: String,
104
+ }
105
+
106
+ #[derive(Debug, Serialize, Deserialize)]
107
+ struct CacheEntry {
108
+ source_hash: String,
109
+ out_rel: String,
110
+ }
111
+
112
+ #[tokio::main]
113
+ async fn main() -> Result<()> {
114
+ let cli = Cli::parse();
115
+ match cli.command {
116
+ Commands::Init { folder } => {
117
+ let dest = std::env::current_dir()?.join(folder);
118
+ init_project(&dest)?;
119
+ }
120
+ Commands::Build {
121
+ folder,
122
+ rojo,
123
+ incremental,
124
+ jobs,
125
+ } => {
126
+ let root = std::env::current_dir()?.join(folder);
127
+ build_project(&root, &rojo, incremental, jobs).await?;
128
+ }
129
+ Commands::Watch { folder, rojo, jobs } => {
130
+ let root = std::env::current_dir()?.join(folder);
131
+ watch_project(root, rojo, jobs).await?;
132
+ }
133
+ Commands::Install { .. } => {
134
+ install_dependencies(std::env::current_dir()?.as_path())?;
135
+ }
136
+ Commands::UpdateTypes { folder } => {
137
+ let root = std::env::current_dir()?.join(folder);
138
+ let status = Command::new("node")
139
+ .arg("generated/cli.js")
140
+ .arg("api")
141
+ .arg("generate")
142
+ .current_dir(&root)
143
+ .status()
144
+ .await
145
+ .context("failed to run node generated/cli.js api generate")?;
146
+ if !status.success() {
147
+ return Err(anyhow!("cluaupp update-types failed"));
148
+ }
149
+ }
150
+ }
151
+ Ok(())
152
+ }
153
+
154
+ fn init_project(dest: &Path) -> Result<()> {
155
+ let home = cluaupp_home()?;
156
+ let template = home.join("templates").join("game");
157
+ let include = home.join("include");
158
+ let runtime = home.join("runtime");
159
+
160
+ copy_dir_all(&template, dest)?;
161
+ copy_dir_all(&include, &dest.join("include"))?;
162
+ if runtime.exists() {
163
+ copy_dir_all(&runtime, &dest.join("libs"))?;
164
+ }
165
+
166
+ let cfg = load_config(dest)?;
167
+ sync_rojo_tree(dest, &cfg, "default.project.json")?;
168
+ println!("Cluaupp ready in {}", dest.display());
169
+ Ok(())
170
+ }
171
+
172
+ fn install_dependencies(root: &Path) -> Result<()> {
173
+ let libs_root = dirs_home()?.join(".clpp").join("libs");
174
+ fs::create_dir_all(&libs_root)?;
175
+ let manifest = root.join("clpp.toml");
176
+ if !manifest.exists() {
177
+ println!("cluaupp install: no clpp.toml, created {}", libs_root.display());
178
+ return Ok(());
179
+ }
180
+ let parsed: ManifestToml = toml::from_str(&fs::read_to_string(&manifest)?)?;
181
+ let deps = parsed.dependencies.unwrap_or_default();
182
+ if deps.is_empty() {
183
+ println!("cluaupp install: no [dependencies] in clpp.toml");
184
+ return Ok(());
185
+ }
186
+ for (name, value) in deps {
187
+ let dep_dir = libs_root.join(&name);
188
+ fs::create_dir_all(&dep_dir)?;
189
+ let marker = dep_dir.join(".source");
190
+ fs::write(marker, value.to_string())?;
191
+ println!("cluaupp install: linked {name}");
192
+ }
193
+ Ok(())
194
+ }
195
+
196
+ async fn build_project(root: &Path, rojo: &str, incremental: bool, jobs: Option<usize>) -> Result<()> {
197
+ let cfg = load_config(root)?;
198
+ let src_dir = root.join(&cfg.root_dir);
199
+ let out_dir = root.join(&cfg.out_dir);
200
+ fs::create_dir_all(&out_dir)?;
201
+ sync_rojo_tree(root, &cfg, rojo)?;
202
+
203
+ let files = collect_sources(&src_dir);
204
+ if files.is_empty() {
205
+ return Err(anyhow!("no .clpp/.clp/.clh files in {}", src_dir.display()));
206
+ }
207
+
208
+ let clpp_bin = resolve_clpp_bin();
209
+ let jobs = jobs.unwrap_or_else(|| std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4));
210
+ let sem = std::sync::Arc::new(Semaphore::new(jobs.max(1)));
211
+
212
+ let mut tasks = Vec::new();
213
+ for file in files {
214
+ let permit = sem.clone().acquire_owned().await?;
215
+ let root = root.to_path_buf();
216
+ let src_dir = src_dir.clone();
217
+ let out_dir = out_dir.clone();
218
+ let cfg = cfg.clone();
219
+ let clpp_bin = clpp_bin.clone();
220
+ let file_path = file.clone();
221
+ let rojo_name = rojo.to_string();
222
+ tasks.push(tokio::spawn(async move {
223
+ let _permit = permit;
224
+ compile_one(&root, &src_dir, &out_dir, &cfg, &clpp_bin, &file_path, incremental, &rojo_name).await
225
+ }));
226
+ }
227
+
228
+ for task in tasks {
229
+ task.await??;
230
+ }
231
+
232
+ sync_rojo_tree(root, &cfg, rojo)?;
233
+ Ok(())
234
+ }
235
+
236
+ async fn watch_project(root: PathBuf, rojo: String, jobs: Option<usize>) -> Result<()> {
237
+ let cfg = load_config(&root)?;
238
+ let src_dir = root.join(&cfg.root_dir);
239
+ if !src_dir.exists() {
240
+ fs::create_dir_all(&src_dir)?;
241
+ }
242
+
243
+ build_project(&root, &rojo, true, jobs).await?;
244
+ println!("cluaupp watch {}", src_dir.display());
245
+
246
+ let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<Event>();
247
+ let mut watcher = RecommendedWatcher::new(
248
+ move |res| {
249
+ if let Ok(event) = res {
250
+ let _ = tx.send(event);
251
+ }
252
+ },
253
+ NotifyConfig::default(),
254
+ )?;
255
+ watcher.watch(&src_dir, RecursiveMode::Recursive)?;
256
+
257
+ while let Some(first) = rx.recv().await {
258
+ if !is_relevant_event(&first.kind) {
259
+ continue;
260
+ }
261
+ let debounce = Duration::from_millis(350);
262
+ loop {
263
+ match tokio::time::timeout(debounce, rx.recv()).await {
264
+ Ok(Some(next)) if is_relevant_event(&next.kind) => continue,
265
+ _ => break,
266
+ }
267
+ }
268
+ if let Err(err) = build_project(&root, &rojo, true, jobs).await {
269
+ eprintln!("{err}");
270
+ }
271
+ }
272
+ Ok(())
273
+ }
274
+
275
+ fn is_relevant_event(kind: &EventKind) -> bool {
276
+ matches!(
277
+ kind,
278
+ EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) | EventKind::Any
279
+ )
280
+ }
281
+
282
+ async fn compile_one(
283
+ root: &Path,
284
+ src_dir: &Path,
285
+ out_dir: &Path,
286
+ cfg: &ProjectConfig,
287
+ clpp_bin: &str,
288
+ file_path: &Path,
289
+ incremental: bool,
290
+ _rojo: &str,
291
+ ) -> Result<()> {
292
+ let rel = normalize(pathdiff(file_path, src_dir)?);
293
+ let source = fs::read_to_string(file_path)?;
294
+ let source_hash = fingerprint_source_with_deps(&source, file_path, src_dir, cfg);
295
+ let out_rel = to_luau_path(&rel);
296
+ let out_file = out_dir.join(&out_rel);
297
+ let cache_file = cache_file_for(root, &rel);
298
+
299
+ if incremental
300
+ && cache_file.exists()
301
+ && out_file.exists()
302
+ && is_cache_hit(&cache_file, &source_hash, &out_rel)?
303
+ {
304
+ return Ok(());
305
+ }
306
+
307
+ let request = json!({
308
+ "source": source,
309
+ "fileName": normalize(file_path.to_path_buf()),
310
+ "strict": cfg.strict,
311
+ "optimize": true
312
+ });
313
+
314
+ let mut cmd = Command::new(clpp_bin);
315
+ cmd.arg("api")
316
+ .arg("compile")
317
+ .stdin(Stdio::piped())
318
+ .stdout(Stdio::piped())
319
+ .stderr(Stdio::piped())
320
+ .current_dir(root);
321
+ let mut child = cmd.spawn().context("failed to spawn clpp api compile")?;
322
+ if let Some(stdin) = child.stdin.as_mut() {
323
+ stdin.write_all(request.to_string().as_bytes()).await?;
324
+ }
325
+ let output = child.wait_with_output().await?;
326
+ if !output.status.success() {
327
+ return Err(anyhow!(
328
+ "{}",
329
+ String::from_utf8_lossy(&output.stderr).trim().to_string()
330
+ ));
331
+ }
332
+
333
+ let stdout = String::from_utf8_lossy(&output.stdout).to_string();
334
+ let artifact: CompileArtifact = serde_json::from_str(stdout.trim())
335
+ .with_context(|| format!("invalid clpp artifact for {rel}"))?;
336
+ if !artifact.ok {
337
+ let message = if !artifact.error.is_empty() {
338
+ artifact.error
339
+ } else {
340
+ format!("clpp compile failed for {}", artifact.file_name)
341
+ };
342
+ return Err(anyhow!(message));
343
+ }
344
+
345
+ let mut luau = artifact.luau;
346
+ luau = rewrite_clpp_libs(luau);
347
+ luau = rewrite_game_rooted_requires(luau);
348
+ luau = ensure_game_services(luau);
349
+ if cfg.strict && !luau.starts_with("--!strict") {
350
+ luau = format!("--!strict\n{luau}");
351
+ }
352
+
353
+ if let Some(parent) = out_file.parent() {
354
+ fs::create_dir_all(parent)?;
355
+ }
356
+ fs::write(&out_file, luau)?;
357
+ write_cache(&cache_file, &CacheEntry { source_hash, out_rel })?;
358
+ println!("cluaupp: {} -> {}", rel, normalize(pathdiff(&out_file, root)?));
359
+ Ok(())
360
+ }
361
+
362
+ fn rewrite_clpp_libs(input: String) -> String {
363
+ let re = Regex::new(r#"require\(ClppLibs\.([A-Za-z_][A-Za-z0-9_]*)\)"#).expect("regex");
364
+ re.replace_all(&input, r#"require(ReplicatedStorage.CluauppLibs.$1)"#)
365
+ .to_string()
366
+ }
367
+
368
+ fn rewrite_game_rooted_requires(input: String) -> String {
369
+ let mut next = input;
370
+ let services = [
371
+ "ReplicatedStorage",
372
+ "ReplicatedFirst",
373
+ "ServerScriptService",
374
+ "ServerStorage",
375
+ "StarterPlayer",
376
+ "StarterGui",
377
+ "Workspace",
378
+ ];
379
+ for svc in services {
380
+ let re = Regex::new(&format!(r"script(?:\.Parent)+\.{}\.", svc)).expect("regex");
381
+ next = re.replace_all(&next, format!("{svc}.")).to_string();
382
+ }
383
+ for folder in ["Include", "Modules", "Declarations"] {
384
+ let re = Regex::new(&format!(r"script(?:\.Parent)+\.{}\.", folder)).expect("regex");
385
+ next = re
386
+ .replace_all(&next, format!("ReplicatedStorage.{folder}."))
387
+ .to_string();
388
+ }
389
+ next
390
+ }
391
+
392
+ fn ensure_game_services(input: String) -> String {
393
+ let mut needed = Vec::new();
394
+ for svc in [
395
+ "ReplicatedStorage",
396
+ "ReplicatedFirst",
397
+ "ServerScriptService",
398
+ "ServerStorage",
399
+ "StarterPlayer",
400
+ ] {
401
+ let need = Regex::new(&format!(r"require\({}\.", svc))
402
+ .expect("regex")
403
+ .is_match(&input);
404
+ let has = Regex::new(&format!(r#"GetService\(\s*"{}"\s*\)"#, svc))
405
+ .expect("regex")
406
+ .is_match(&input);
407
+ if need && !has {
408
+ needed.push(svc);
409
+ }
410
+ }
411
+ if needed.is_empty() {
412
+ return input;
413
+ }
414
+
415
+ let block = needed
416
+ .iter()
417
+ .map(|svc| format!(r#"const {} = game:GetService("{}")"#, svc, svc))
418
+ .collect::<Vec<_>>()
419
+ .join("\n");
420
+
421
+ let mut lines = input.lines().collect::<Vec<_>>();
422
+ let mut insert_at = 0usize;
423
+ while insert_at < lines.len() && lines[insert_at].starts_with("--") {
424
+ insert_at += 1;
425
+ }
426
+ lines.insert(insert_at, "");
427
+ lines.insert(insert_at + 1, &block);
428
+ lines.join("\n")
429
+ }
430
+
431
+ fn is_cache_hit(cache_file: &Path, source_hash: &str, out_rel: &str) -> Result<bool> {
432
+ let text = fs::read_to_string(cache_file)?;
433
+ let cached: CacheEntry = serde_json::from_str(&text)?;
434
+ Ok(cached.source_hash == source_hash && cached.out_rel == out_rel)
435
+ }
436
+
437
+ fn write_cache(path: &Path, entry: &CacheEntry) -> Result<()> {
438
+ if let Some(parent) = path.parent() {
439
+ fs::create_dir_all(parent)?;
440
+ }
441
+ fs::write(path, serde_json::to_vec(entry)?)?;
442
+ Ok(())
443
+ }
444
+
445
+ fn cache_file_for(root: &Path, rel: &str) -> PathBuf {
446
+ let key = hex_sha(rel.as_bytes());
447
+ root.join(".cluaupp")
448
+ .join("compile-cache")
449
+ .join(format!("{key}.json"))
450
+ }
451
+
452
+ fn fingerprint_source_with_deps(source: &str, file_path: &Path, src_dir: &Path, cfg: &ProjectConfig) -> String {
453
+ let mut hasher = Sha256::new();
454
+ hasher.update(cfg.strict.to_string().as_bytes());
455
+ hasher.update(source.as_bytes());
456
+ for dep in resolve_language_deps(source, file_path, src_dir) {
457
+ hasher.update(dep.to_string_lossy().as_bytes());
458
+ if let Ok(content) = fs::read(&dep) {
459
+ hasher.update(content);
460
+ }
461
+ }
462
+ hex_bytes(&hasher.finalize())
463
+ }
464
+
465
+ fn resolve_language_deps(source: &str, file_path: &Path, src_dir: &Path) -> Vec<PathBuf> {
466
+ let mut deps = Vec::new();
467
+ let mut seen = HashSet::new();
468
+ let link_re = Regex::new(r#"(?m)^\s*link\s+["']([^"']+)["'](?:\s+as\s+[A-Za-z_][A-Za-z0-9_]*)?\s*;?\s*$"#)
469
+ .expect("regex");
470
+
471
+ for cap in link_re.captures_iter(source) {
472
+ if let Some(path_match) = cap.get(1) {
473
+ if let Some(dep) = resolve_dep(path_match.as_str(), file_path, src_dir) {
474
+ let key = normalize(dep.clone());
475
+ if seen.insert(key) {
476
+ deps.push(dep);
477
+ }
478
+ }
479
+ }
480
+ }
481
+ deps
482
+ }
483
+
484
+ fn resolve_dep(raw: &str, from_file: &Path, src_dir: &Path) -> Option<PathBuf> {
485
+ let from_dir = from_file.parent()?;
486
+ let candidates = [
487
+ from_dir.join(raw),
488
+ src_dir.join(raw),
489
+ from_dir.join(format!("{raw}.clh")),
490
+ src_dir.join(format!("{raw}.clh")),
491
+ ];
492
+ candidates.into_iter().find(|p| p.is_file())
493
+ }
494
+
495
+ fn collect_sources(root: &Path) -> Vec<PathBuf> {
496
+ WalkDir::new(root)
497
+ .into_iter()
498
+ .filter_map(|e| e.ok())
499
+ .filter(|e| e.file_type().is_file())
500
+ .map(|e| e.into_path())
501
+ .filter(|p| is_source_file(p.as_path()))
502
+ .collect()
503
+ }
504
+
505
+ fn is_source_file(path: &Path) -> bool {
506
+ matches!(
507
+ path.extension().and_then(OsStr::to_str).map(|v| v.to_ascii_lowercase()),
508
+ Some(ext) if ext == "clpp" || ext == "clp" || ext == "clh"
509
+ )
510
+ }
511
+
512
+ fn to_luau_path(rel: &str) -> String {
513
+ Regex::new(r"\.(clpp|clp|clh)$")
514
+ .expect("regex")
515
+ .replace(rel, ".luau")
516
+ .to_string()
517
+ }
518
+
519
+ fn load_config(root: &Path) -> Result<ProjectConfig> {
520
+ let manifest = root.join("clpp.toml");
521
+ if manifest.exists() {
522
+ let parsed: ManifestToml = toml::from_str(&fs::read_to_string(&manifest)?)?;
523
+ if let Some(compiler) = parsed.compiler {
524
+ return Ok(ProjectConfig {
525
+ root_dir: compiler.root_dir.unwrap_or_else(default_root_dir),
526
+ out_dir: compiler.out_dir.unwrap_or_else(default_out_dir),
527
+ strict: compiler.strict.unwrap_or(false),
528
+ });
529
+ }
530
+ }
531
+ let cfg1 = root.join("cluaupp.config.json");
532
+ let cfg2 = root.join("cluau.config.json");
533
+ if cfg1.exists() {
534
+ let parsed: ProjectConfig = serde_json::from_str(&fs::read_to_string(&cfg1)?)?;
535
+ return Ok(parsed);
536
+ }
537
+ if cfg2.exists() {
538
+ let parsed: ProjectConfig = serde_json::from_str(&fs::read_to_string(&cfg2)?)?;
539
+ return Ok(parsed);
540
+ }
541
+ Ok(ProjectConfig {
542
+ root_dir: default_root_dir(),
543
+ out_dir: default_out_dir(),
544
+ strict: false,
545
+ })
546
+ }
547
+
548
+ fn sync_rojo_tree(root: &Path, cfg: &ProjectConfig, rojo_file: &str) -> Result<()> {
549
+ let tree = build_rojo_tree(root, &cfg.root_dir, &cfg.out_dir)?;
550
+ let doc = json!({
551
+ "name": "cluaupp-game",
552
+ "tree": tree
553
+ });
554
+ fs::write(root.join(rojo_file), format!("{}\n", serde_json::to_string_pretty(&doc)?))?;
555
+ Ok(())
556
+ }
557
+
558
+ fn build_rojo_tree(root: &Path, root_dir: &str, out_dir: &str) -> Result<serde_json::Value> {
559
+ let out_root = root.join(out_dir);
560
+ let src_root = root.join(root_dir);
561
+ let source_root = if out_root.exists() { out_root } else { src_root };
562
+ let rel_root = normalize(pathdiff(&source_root, root)?);
563
+
564
+ let mut tree = json!({
565
+ "$className": "DataModel",
566
+ "ServerScriptService": { "$className": "ServerScriptService", "$path": format!("{}/Server", rel_root) },
567
+ "ReplicatedStorage": {
568
+ "$className": "ReplicatedStorage",
569
+ "CluauppLibs": { "$path": "libs" },
570
+ "Declarations": { "$path": format!("{}/Declarations", rel_root) },
571
+ "Include": { "$path": format!("{}/Include", rel_root) },
572
+ },
573
+ "StarterPlayer": {
574
+ "$className": "StarterPlayer",
575
+ "StarterPlayerScripts": {
576
+ "$className": "StarterPlayerScripts",
577
+ "$path": format!("{}/Client", rel_root)
578
+ }
579
+ },
580
+ "HttpService": {
581
+ "$className": "HttpService",
582
+ "$properties": { "HttpEnabled": true }
583
+ }
584
+ });
585
+
586
+ tree["ServerScriptService"]["Parallel"] = json!({ "$path": format!("{}/Parallel", rel_root) });
587
+ tree["ServerScriptService"]["Modules"] = json!({});
588
+ tree["StarterPlayer"]["StarterPlayerScripts"]["Modules"] = json!({});
589
+ tree["ReplicatedStorage"]["Modules"] = json!({});
590
+
591
+ let modules_root = source_root.join("Modules");
592
+ if modules_root.exists() {
593
+ for entry in WalkDir::new(&modules_root).into_iter().filter_map(|e| e.ok()) {
594
+ if !entry.file_type().is_file() {
595
+ continue;
596
+ }
597
+ let file = entry.path();
598
+ let ext = file.extension().and_then(OsStr::to_str).unwrap_or_default().to_ascii_lowercase();
599
+ if !["luau", "lua", "clpp", "clp", "clh"].contains(&ext.as_str()) {
600
+ continue;
601
+ }
602
+ let module_rel = normalize(pathdiff(file, &modules_root)?);
603
+ let stem = module_stem(&module_rel);
604
+ if stem.is_empty() {
605
+ continue;
606
+ }
607
+ let out_rel = normalize(pathdiff(file, root)?);
608
+ let is_server = Regex::new(r"\.server\.(luau|lua|clpp|clp|clh)$")
609
+ .expect("regex")
610
+ .is_match(&module_rel);
611
+ let is_client = Regex::new(r"\.client\.(luau|lua|clpp|clp|clh)$")
612
+ .expect("regex")
613
+ .is_match(&module_rel);
614
+ let target = if is_server {
615
+ &mut tree["ServerScriptService"]["Modules"]
616
+ } else if is_client {
617
+ &mut tree["StarterPlayer"]["StarterPlayerScripts"]["Modules"]
618
+ } else {
619
+ &mut tree["ReplicatedStorage"]["Modules"]
620
+ };
621
+ set_nested_path(target, &stem, &out_rel);
622
+ }
623
+ }
624
+ Ok(tree)
625
+ }
626
+
627
+ fn module_stem(rel: &str) -> String {
628
+ let re = Regex::new(r"(\.server|\.client)?\.(luau|lua|clpp|clp|clh)$").expect("regex");
629
+ re.replace(rel, "").to_string()
630
+ }
631
+
632
+ fn set_nested_path(node: &mut serde_json::Value, stem: &str, fs_path: &str) {
633
+ let parts: Vec<&str> = stem.split('/').filter(|p| !p.is_empty()).collect();
634
+ if parts.is_empty() {
635
+ return;
636
+ }
637
+ let mut cur = node;
638
+ for part in &parts[..parts.len() - 1] {
639
+ if cur.get(*part).is_none() {
640
+ cur[*part] = json!({});
641
+ }
642
+ cur = &mut cur[*part];
643
+ }
644
+ cur[parts[parts.len() - 1]] = json!({ "$path": fs_path });
645
+ }
646
+
647
+ fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
648
+ if !src.exists() {
649
+ return Ok(());
650
+ }
651
+ for entry in WalkDir::new(src).into_iter().filter_map(|e| e.ok()) {
652
+ let rel = entry.path().strip_prefix(src).unwrap_or(entry.path());
653
+ let target = dst.join(rel);
654
+ if entry.file_type().is_dir() {
655
+ fs::create_dir_all(&target)?;
656
+ } else if entry.file_type().is_file() {
657
+ if let Some(parent) = target.parent() {
658
+ fs::create_dir_all(parent)?;
659
+ }
660
+ fs::copy(entry.path(), &target)?;
661
+ }
662
+ }
663
+ Ok(())
664
+ }
665
+
666
+ fn resolve_clpp_bin() -> String {
667
+ std::env::var("CLPP")
668
+ .or_else(|_| std::env::var("CLPP_PATH"))
669
+ .unwrap_or_else(|_| "clpp".to_string())
670
+ }
671
+
672
+ fn cluaupp_home() -> Result<PathBuf> {
673
+ if let Ok(home) = std::env::var("CLUAUPP_HOME") {
674
+ return Ok(PathBuf::from(home));
675
+ }
676
+ let exe = std::env::current_exe()?;
677
+ let base = exe
678
+ .parent()
679
+ .and_then(|p| p.parent())
680
+ .map(|p| p.to_path_buf())
681
+ .ok_or_else(|| anyhow!("failed to resolve CLUAUPP_HOME"))?;
682
+ Ok(base)
683
+ }
684
+
685
+ fn dirs_home() -> Result<PathBuf> {
686
+ if let Ok(home) = std::env::var("HOME") {
687
+ return Ok(PathBuf::from(home));
688
+ }
689
+ if let Ok(home) = std::env::var("USERPROFILE") {
690
+ return Ok(PathBuf::from(home));
691
+ }
692
+ Err(anyhow!("failed to resolve home directory"))
693
+ }
694
+
695
+ fn pathdiff(path: &Path, base: &Path) -> Result<PathBuf> {
696
+ path.strip_prefix(base)
697
+ .map(|p| p.to_path_buf())
698
+ .map_err(|_| anyhow!("failed relative path"))
699
+ }
700
+
701
+ fn normalize(path: PathBuf) -> String {
702
+ path.to_string_lossy().replace('\\', "/")
703
+ }
704
+
705
+ fn hex_sha(bytes: &[u8]) -> String {
706
+ let mut hasher = Sha256::new();
707
+ hasher.update(bytes);
708
+ hex_bytes(&hasher.finalize())
709
+ }
710
+
711
+ fn hex_bytes(bytes: &[u8]) -> String {
712
+ let mut out = String::with_capacity(bytes.len() * 2);
713
+ for b in bytes {
714
+ out.push_str(&format!("{:02x}", b));
715
+ }
716
+ out
717
+ }