1use std::{path::Path, str};
2
3use anyhow::Context as _;
4use memofs::Vfs;
5use rbx_dom_weak::{
6 types::{Enum, Variant},
7 ustr, HashMapExt as _, UstrMap,
8};
9
10use crate::{
11 snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
12 syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot},
13};
14
15use super::{
16 dir::{snapshot_dir_no_meta, syncback_dir_no_meta},
17 meta_file::{AdjacentMetadata, DirectoryMetadata},
18 PathExt as _,
19};
20
21#[derive(Debug)]
22pub enum ScriptType {
23 Server,
24 Client,
25 Module,
26 Plugin,
27 LegacyServer,
28 LegacyClient,
29 RunContextServer,
30 RunContextClient,
31}
32
33pub fn snapshot_lua(
35 context: &InstanceContext,
36 vfs: &Vfs,
37 path: &Path,
38 name: &str,
39 script_type: ScriptType,
40) -> anyhow::Result<Option<InstanceSnapshot>> {
41 let run_context_enums = &rbx_reflection_database::get()
42 .unwrap()
43 .enums
44 .get("RunContext")
45 .expect("Unable to get RunContext enums!")
46 .items;
47
48 let (class_name, run_context) = match script_type {
49 ScriptType::Server => {
50 if context.emit_legacy_scripts {
51 ("Script", run_context_enums.get("Legacy"))
52 } else {
53 ("Script", run_context_enums.get("Server"))
54 }
55 }
56 ScriptType::Client => {
57 if context.emit_legacy_scripts {
58 ("LocalScript", None)
59 } else {
60 ("Script", run_context_enums.get("Client"))
61 }
62 }
63 ScriptType::Module => ("ModuleScript", None),
64 ScriptType::Plugin => ("Script", run_context_enums.get("Plugin")),
65 ScriptType::LegacyServer => ("Script", run_context_enums.get("Legacy")),
66 ScriptType::LegacyClient => ("LocalScript", None),
67 ScriptType::RunContextServer => ("Script", run_context_enums.get("Server")),
68 ScriptType::RunContextClient => ("Script", run_context_enums.get("Client")),
69 };
70
71 let contents = vfs.read_to_string_lf_normalized(path)?;
72 let contents_str = contents.as_str();
73
74 let mut properties = UstrMap::with_capacity(2);
75 properties.insert(ustr("Source"), contents_str.into());
76
77 if let Some(run_context) = run_context {
78 properties.insert(
79 ustr("RunContext"),
80 Enum::from_u32(run_context.to_owned()).into(),
81 );
82 }
83
84 let mut snapshot = InstanceSnapshot::new()
85 .name(name)
86 .class_name(class_name)
87 .properties(properties)
88 .metadata(
89 InstanceMetadata::new()
90 .instigating_source(path)
91 .relevant_paths(vec![vfs.canonicalize(path)?])
92 .context(context),
93 );
94
95 AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?;
96
97 Ok(Some(snapshot))
98}
99
100pub fn snapshot_lua_init(
106 context: &InstanceContext,
107 vfs: &Vfs,
108 init_path: &Path,
109 name: &str,
110 script_type: ScriptType,
111) -> anyhow::Result<Option<InstanceSnapshot>> {
112 let folder_path = init_path.parent().unwrap();
113 let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path, name)?.unwrap();
114
115 if dir_snapshot.class_name != "Folder" {
116 anyhow::bail!(
117 "init.lua, init.server.lua, and init.client.lua can \
118 only be used if the instance produced by the containing \
119 directory would be a Folder.\n\
120 \n\
121 The directory {} turned into an instance of class {}.",
122 folder_path.display(),
123 dir_snapshot.class_name
124 );
125 }
126
127 let mut init_snapshot =
128 snapshot_lua(context, vfs, init_path, &dir_snapshot.name, script_type)?.unwrap();
129
130 init_snapshot.children = dir_snapshot.children;
131 init_snapshot.metadata = dir_snapshot.metadata;
132 DirectoryMetadata::read_and_apply_all(vfs, folder_path, &mut init_snapshot)?;
136
137 Ok(Some(init_snapshot))
138}
139
140pub fn syncback_lua<'sync>(
141 snapshot: &SyncbackSnapshot<'sync>,
142) -> anyhow::Result<SyncbackReturn<'sync>> {
143 let new_inst = snapshot.new_inst();
144
145 let contents = if let Some(Variant::String(source)) = new_inst.properties.get(&ustr("Source")) {
146 source.as_bytes().to_vec()
147 } else {
148 anyhow::bail!("Scripts must have a `Source` property that is a String")
149 };
150 let mut fs_snapshot = FsSnapshot::new();
151 fs_snapshot.add_file(&snapshot.path, contents);
152
153 let meta = AdjacentMetadata::from_syncback_snapshot(snapshot, snapshot.path.clone())?;
154 if let Some(mut meta) = meta {
155 meta.properties.shift_remove(&ustr("Source"));
158
159 if !meta.is_empty() {
160 let parent_location = snapshot.path.parent_err()?;
161 fs_snapshot.add_file(
162 parent_location.join(format!("{}.meta.json", new_inst.name)),
163 serde_json::to_vec_pretty(&meta).context("cannot serialize metadata")?,
164 );
165 }
166 }
167
168 Ok(SyncbackReturn {
169 fs_snapshot,
170 children: Vec::new(),
172 removed_children: Vec::new(),
173 })
174}
175
176pub fn syncback_lua_init<'sync>(
177 script_type: ScriptType,
178 snapshot: &SyncbackSnapshot<'sync>,
179) -> anyhow::Result<SyncbackReturn<'sync>> {
180 let new_inst = snapshot.new_inst();
181 let path = snapshot.path.join(match script_type {
182 ScriptType::Server => "init.server.luau",
183 ScriptType::Client => "init.client.luau",
184 ScriptType::Module => "init.luau",
185 ScriptType::Plugin => "init.plugin.luau",
186 _ => anyhow::bail!("syncback is not yet implemented for {script_type:?}"),
187 });
188
189 let contents = if let Some(Variant::String(source)) = new_inst.properties.get(&ustr("Source")) {
190 source.as_bytes().to_vec()
191 } else {
192 anyhow::bail!("Scripts must have a `Source` property that is a String")
193 };
194
195 let mut dir_syncback = syncback_dir_no_meta(snapshot)?;
196 dir_syncback.fs_snapshot.add_file(&path, contents);
197
198 let meta = DirectoryMetadata::from_syncback_snapshot(snapshot, path.clone())?;
199 if let Some(mut meta) = meta {
200 meta.properties.shift_remove(&ustr("Source"));
203
204 if !meta.is_empty() {
205 dir_syncback.fs_snapshot.add_file(
206 snapshot.path.join("init.meta.json"),
207 serde_json::to_vec_pretty(&meta)
208 .context("could not serialize new init.meta.json")?,
209 );
210 }
211 }
212
213 Ok(dir_syncback)
214}
215
216#[cfg(test)]
217mod test {
218 use super::*;
219
220 use memofs::{InMemoryFs, VfsSnapshot};
221
222 #[test]
223 fn class_module_from_vfs() {
224 let mut imfs = InMemoryFs::new();
225 imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
226 .unwrap();
227
228 let vfs = Vfs::new(imfs);
229
230 let instance_snapshot = snapshot_lua(
231 &InstanceContext::with_emit_legacy_scripts(Some(true)),
232 &vfs,
233 Path::new("/foo.lua"),
234 "foo",
235 ScriptType::Module,
236 )
237 .unwrap()
238 .unwrap();
239
240 insta::with_settings!({ sort_maps => true }, {
241 insta::assert_yaml_snapshot!(instance_snapshot);
242 });
243 }
244
245 #[test]
246 fn runcontext_module_from_vfs() {
247 let mut imfs = InMemoryFs::new();
248 imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
249 .unwrap();
250
251 let vfs = Vfs::new(imfs);
252
253 let instance_snapshot = snapshot_lua(
254 &InstanceContext::with_emit_legacy_scripts(Some(false)),
255 &vfs,
256 Path::new("/foo.lua"),
257 "foo",
258 ScriptType::Module,
259 )
260 .unwrap()
261 .unwrap();
262
263 insta::with_settings!({ sort_maps => true }, {
264 insta::assert_yaml_snapshot!(instance_snapshot);
265 });
266 }
267
268 #[test]
269 fn plugin_module_from_vfs() {
270 let mut imfs = InMemoryFs::new();
271 imfs.load_snapshot("/foo.plugin.lua", VfsSnapshot::file("Hello there!"))
272 .unwrap();
273
274 let vfs = Vfs::new(imfs);
275
276 let instance_snapshot = snapshot_lua(
277 &InstanceContext::with_emit_legacy_scripts(Some(false)),
278 &vfs,
279 Path::new("/foo.plugin.lua"),
280 "foo",
281 ScriptType::Plugin,
282 )
283 .unwrap()
284 .unwrap();
285
286 insta::with_settings!({ sort_maps => true }, {
287 insta::assert_yaml_snapshot!(instance_snapshot);
288 });
289 }
290
291 #[test]
292 fn class_server_from_vfs() {
293 let mut imfs = InMemoryFs::new();
294 imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
295 .unwrap();
296
297 let vfs = Vfs::new(imfs);
298
299 let instance_snapshot = snapshot_lua(
300 &InstanceContext::with_emit_legacy_scripts(Some(true)),
301 &vfs,
302 Path::new("/foo.server.lua"),
303 "foo",
304 ScriptType::Server,
305 )
306 .unwrap()
307 .unwrap();
308
309 insta::with_settings!({ sort_maps => true }, {
310 insta::assert_yaml_snapshot!(instance_snapshot);
311 });
312 }
313
314 #[test]
315 fn runcontext_server_from_vfs() {
316 let mut imfs = InMemoryFs::new();
317 imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
318 .unwrap();
319
320 let vfs = Vfs::new(imfs);
321
322 let instance_snapshot = snapshot_lua(
323 &InstanceContext::with_emit_legacy_scripts(Some(false)),
324 &vfs,
325 Path::new("/foo.server.lua"),
326 "foo",
327 ScriptType::Server,
328 )
329 .unwrap()
330 .unwrap();
331
332 insta::with_settings!({ sort_maps => true }, {
333 insta::assert_yaml_snapshot!(instance_snapshot);
334 });
335 }
336
337 #[test]
338 fn class_client_from_vfs() {
339 let mut imfs = InMemoryFs::new();
340 imfs.load_snapshot("/foo.client.lua", VfsSnapshot::file("Hello there!"))
341 .unwrap();
342
343 let vfs = Vfs::new(imfs);
344
345 let instance_snapshot = snapshot_lua(
346 &InstanceContext::with_emit_legacy_scripts(Some(true)),
347 &vfs,
348 Path::new("/foo.client.lua"),
349 "foo",
350 ScriptType::Client,
351 )
352 .unwrap()
353 .unwrap();
354
355 insta::with_settings!({ sort_maps => true }, {
356 insta::assert_yaml_snapshot!(instance_snapshot);
357 });
358 }
359
360 #[test]
361 fn runcontext_client_from_vfs() {
362 let mut imfs = InMemoryFs::new();
363 imfs.load_snapshot("/foo.client.lua", VfsSnapshot::file("Hello there!"))
364 .unwrap();
365
366 let vfs = Vfs::new(imfs);
367
368 let instance_snapshot = snapshot_lua(
369 &InstanceContext::with_emit_legacy_scripts(Some(false)),
370 &vfs,
371 Path::new("/foo.client.lua"),
372 "foo",
373 ScriptType::Client,
374 )
375 .unwrap()
376 .unwrap();
377
378 insta::with_settings!({ sort_maps => true }, {
379 insta::assert_yaml_snapshot!(instance_snapshot);
380 });
381 }
382
383 #[test]
384 fn init_module_from_vfs() {
385 let mut imfs = InMemoryFs::new();
386 imfs.load_snapshot(
387 "/root",
388 VfsSnapshot::dir([("init.lua", VfsSnapshot::file("Hello!"))]),
389 )
390 .unwrap();
391
392 let vfs = Vfs::new(imfs);
393
394 let instance_snapshot = snapshot_lua_init(
395 &InstanceContext::with_emit_legacy_scripts(Some(true)),
396 &vfs,
397 Path::new("/root/init.lua"),
398 "root",
399 ScriptType::Module,
400 )
401 .unwrap()
402 .unwrap();
403
404 insta::with_settings!({ sort_maps => true }, {
405 insta::assert_yaml_snapshot!(instance_snapshot);
406 });
407 }
408
409 #[test]
410 fn init_module_from_vfs_with_meta() {
411 let mut imfs = InMemoryFs::new();
412 imfs.load_snapshot(
413 "/root",
414 VfsSnapshot::dir([
415 ("init.lua", VfsSnapshot::file("Hello!")),
416 (
417 "init.meta.json",
418 VfsSnapshot::file(r#"{"id": "manually specified"}"#),
419 ),
420 ]),
421 )
422 .unwrap();
423
424 let vfs = Vfs::new(imfs);
425
426 let instance_snapshot = snapshot_lua_init(
427 &InstanceContext::with_emit_legacy_scripts(Some(true)),
428 &vfs,
429 Path::new("/root/init.lua"),
430 "root",
431 ScriptType::Module,
432 )
433 .unwrap()
434 .unwrap();
435
436 insta::with_settings!({ sort_maps => true }, {
437 insta::assert_yaml_snapshot!(instance_snapshot);
438 });
439 }
440
441 #[test]
442 fn class_module_with_meta() {
443 let mut imfs = InMemoryFs::new();
444 imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
445 .unwrap();
446 imfs.load_snapshot(
447 "/foo.meta.json",
448 VfsSnapshot::file(
449 r#"
450 {
451 "ignoreUnknownInstances": true
452 }
453 "#,
454 ),
455 )
456 .unwrap();
457
458 let vfs = Vfs::new(imfs);
459
460 let instance_snapshot = snapshot_lua(
461 &InstanceContext::with_emit_legacy_scripts(Some(true)),
462 &vfs,
463 Path::new("/foo.lua"),
464 "foo",
465 ScriptType::Module,
466 )
467 .unwrap()
468 .unwrap();
469
470 insta::with_settings!({ sort_maps => true }, {
471 insta::assert_yaml_snapshot!(instance_snapshot);
472 });
473 }
474
475 #[test]
476 fn runcontext_module_with_meta() {
477 let mut imfs = InMemoryFs::new();
478 imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
479 .unwrap();
480 imfs.load_snapshot(
481 "/foo.meta.json",
482 VfsSnapshot::file(
483 r#"
484 {
485 "ignoreUnknownInstances": true
486 }
487 "#,
488 ),
489 )
490 .unwrap();
491
492 let vfs = Vfs::new(imfs);
493
494 let instance_snapshot = snapshot_lua(
495 &InstanceContext::with_emit_legacy_scripts(Some(false)),
496 &vfs,
497 Path::new("/foo.lua"),
498 "foo",
499 ScriptType::Module,
500 )
501 .unwrap()
502 .unwrap();
503
504 insta::with_settings!({ sort_maps => true }, {
505 insta::assert_yaml_snapshot!(instance_snapshot);
506 });
507 }
508
509 #[test]
510 fn class_script_with_meta() {
511 let mut imfs = InMemoryFs::new();
512 imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
513 .unwrap();
514 imfs.load_snapshot(
515 "/foo.meta.json",
516 VfsSnapshot::file(
517 r#"
518 {
519 "ignoreUnknownInstances": true
520 }
521 "#,
522 ),
523 )
524 .unwrap();
525
526 let vfs = Vfs::new(imfs);
527
528 let instance_snapshot = snapshot_lua(
529 &InstanceContext::with_emit_legacy_scripts(Some(true)),
530 &vfs,
531 Path::new("/foo.server.lua"),
532 "foo",
533 ScriptType::Server,
534 )
535 .unwrap()
536 .unwrap();
537
538 insta::with_settings!({ sort_maps => true }, {
539 insta::assert_yaml_snapshot!(instance_snapshot);
540 });
541 }
542
543 #[test]
544 fn runcontext_script_with_meta() {
545 let mut imfs = InMemoryFs::new();
546 imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
547 .unwrap();
548 imfs.load_snapshot(
549 "/foo.meta.json",
550 VfsSnapshot::file(
551 r#"
552 {
553 "ignoreUnknownInstances": true
554 }
555 "#,
556 ),
557 )
558 .unwrap();
559
560 let vfs = Vfs::new(imfs);
561
562 let instance_snapshot = snapshot_lua(
563 &InstanceContext::with_emit_legacy_scripts(Some(false)),
564 &vfs,
565 Path::new("/foo.server.lua"),
566 "foo",
567 ScriptType::Server,
568 )
569 .unwrap()
570 .unwrap();
571
572 insta::with_settings!({ sort_maps => true }, {
573 insta::assert_yaml_snapshot!(instance_snapshot);
574 });
575 }
576
577 #[test]
578 fn class_script_disabled() {
579 let mut imfs = InMemoryFs::new();
580 imfs.load_snapshot("/bar.server.lua", VfsSnapshot::file("Hello there!"))
581 .unwrap();
582 imfs.load_snapshot(
583 "/bar.meta.json",
584 VfsSnapshot::file(
585 r#"
586 {
587 "properties": {
588 "Disabled": true
589 }
590 }
591 "#,
592 ),
593 )
594 .unwrap();
595
596 let vfs = Vfs::new(imfs);
597
598 let instance_snapshot = snapshot_lua(
599 &InstanceContext::with_emit_legacy_scripts(Some(true)),
600 &vfs,
601 Path::new("/bar.server.lua"),
602 "bar",
603 ScriptType::Server,
604 )
605 .unwrap()
606 .unwrap();
607
608 insta::with_settings!({ sort_maps => true }, {
609 insta::assert_yaml_snapshot!(instance_snapshot);
610 });
611 }
612
613 #[test]
614 fn runcontext_script_disabled() {
615 let mut imfs = InMemoryFs::new();
616 imfs.load_snapshot("/bar.server.lua", VfsSnapshot::file("Hello there!"))
617 .unwrap();
618 imfs.load_snapshot(
619 "/bar.meta.json",
620 VfsSnapshot::file(
621 r#"
622 {
623 "properties": {
624 "Disabled": true
625 }
626 }
627 "#,
628 ),
629 )
630 .unwrap();
631
632 let vfs = Vfs::new(imfs);
633
634 let instance_snapshot = snapshot_lua(
635 &InstanceContext::with_emit_legacy_scripts(Some(false)),
636 &vfs,
637 Path::new("/bar.server.lua"),
638 "bar",
639 ScriptType::Server,
640 )
641 .unwrap()
642 .unwrap();
643
644 insta::with_settings!({ sort_maps => true }, {
645 insta::assert_yaml_snapshot!(instance_snapshot);
646 });
647 }
648}