dalbit_core/
transpile.rs

1use std::{ffi::OsStr, path::PathBuf, str::FromStr};
2
3use anyhow::{anyhow, Result};
4use async_walkdir::WalkDir;
5use darklua_core::{
6    rules::{self, bundle::BundleRequireMode},
7    BundleConfiguration, Configuration, GeneratorParameters, Options, Resources,
8};
9use futures_lite::stream::StreamExt;
10use indexmap::IndexMap;
11use tokio::fs;
12
13use crate::{injector::Injector, manifest::Manifest, modifiers::Modifier, utils};
14
15pub const DALBIT_GLOBAL_IDENTIFIER_PREFIX: &str = "DALBIT_";
16
17pub const DEFAULT_LUAU_TO_LUA_MODIFIERS: [&str; 8] = [
18    "remove_interpolated_string",
19    "remove_compound_assignment",
20    "remove_types",
21    "remove_if_expression",
22    "remove_continue",
23    "remove_redeclared_keys",
24    "remove_generalized_iteration",
25    "remove_number_literals",
26];
27
28pub const DEFAULT_MINIFYING_MODIFIERS: [&str; 11] = [
29    "remove_spaces",
30    "remove_nil_declaration",
31    "remove_function_call_parens",
32    "remove_comments",
33    "convert_index_to_field",
34    "compute_expression",
35    "filter_after_early_return",
36    "remove_unused_variable",
37    "remove_unused_while",
38    "remove_unused_if_branch",
39    "remove_empty_do",
40];
41
42async fn private_process(
43    manifest: &Manifest,
44    input: &PathBuf,
45    output: &PathBuf,
46    additional_modifiers: Option<&mut Vec<Modifier>>,
47    bundle: bool,
48) -> Result<Vec<PathBuf>> {
49    let resources = Resources::from_file_system();
50
51    let mut modifiers = Vec::new();
52    if let Some(additional_modifiers) = additional_modifiers {
53        modifiers.append(additional_modifiers);
54    }
55    {
56        let mut transpiling_modifiers = IndexMap::new();
57        for name in DEFAULT_LUAU_TO_LUA_MODIFIERS {
58            transpiling_modifiers.insert(name, true);
59        }
60        for (name, enabled) in manifest.modifiers() {
61            let name = name.as_str();
62            log::debug!("inserted modifier name: {}", name);
63            transpiling_modifiers.insert(name, *enabled);
64        }
65        for (name, enabled) in transpiling_modifiers {
66            let modifier = Modifier::from_str(name)?;
67            if enabled {
68                modifiers.push(modifier);
69            }
70        }
71    }
72    if manifest.minify {
73        for name in DEFAULT_MINIFYING_MODIFIERS {
74            modifiers.push(Modifier::from_str(name)?);
75        }
76    }
77
78    let (rules, mut fullmoon_visitors) = modifiers.into_iter().fold(
79        (Vec::new(), Vec::new()),
80        |(mut rules, mut fullmoon_visitors), modifier| {
81            match modifier {
82                Modifier::DarkluaRule(darklua_rule) => rules.push(darklua_rule),
83                Modifier::FullMoonVisitor(fullmoon_visitor) => {
84                    fullmoon_visitors.push(fullmoon_visitor)
85                }
86            }
87            (rules, fullmoon_visitors)
88        },
89    );
90
91    let mut options = Options::new(input).with_configuration({
92        // let mut config: Configuration = if bundle {
93        //     toml::from_str("bundle = { require_mode = 'path' }").unwrap()
94        // } else {
95        //     Configuration::empty()
96        // };
97        let mut config = Configuration::empty();
98        if bundle {
99            config = config
100                .with_bundle_configuration(BundleConfiguration::new(BundleRequireMode::default()));
101        }
102        config = config.with_generator(GeneratorParameters::RetainLines);
103
104        rules
105            .into_iter()
106            .fold(config, |config, rule| config.with_rule(rule))
107    });
108    options = options.with_output(&output);
109    let result = darklua_core::process(&resources, options).map_err(|e| anyhow!(e))?;
110
111    let success_count = result.success_count();
112    let errors = result.collect_errors();
113    let error_count = errors.len();
114    if error_count > 0 {
115        eprintln!(
116            "{}{} error{} happened:",
117            if success_count > 0 { "but " } else { "" },
118            error_count,
119            if error_count > 1 { "s" } else { "" }
120        );
121
122        errors
123            .into_iter()
124            .for_each(|error| eprintln!("-> {}", error));
125
126        return Err(anyhow!("darklua process was not successful"));
127    }
128
129    let mut created_files: Vec<PathBuf> = if output.is_dir() {
130        let mut created_files = Vec::new();
131        let mut entries = WalkDir::new(output);
132        while let Some(entry) = entries.next().await {
133            let path = entry?.path();
134            if !matches!(
135                path.extension().and_then(OsStr::to_str),
136                Some("lua") | Some("luau")
137            ) {
138                continue;
139            }
140            created_files.push(path);
141        }
142        created_files
143    } else {
144        vec![output.clone()]
145    };
146    println!("created_files: {:?}", created_files);
147
148    let extension = manifest.file_extension();
149    if fullmoon_visitors.is_empty() {
150        if let Some(extension) = extension {
151            for path in &mut created_files {
152                let old_path = path.clone();
153                path.set_extension(extension);
154                fs::rename(old_path, path).await?;
155            }
156        }
157    } else {
158        for path in &mut created_files {
159            let mut ast = utils::parse_file(path, manifest.target_version()).await?;
160
161            for visitor in &mut fullmoon_visitors {
162                ast = visitor.visit_ast_boxed(ast);
163            }
164
165            if let Some(extension) = extension {
166                let old_path = path.clone();
167                path.set_extension(extension);
168                let new_path = path.to_owned();
169                if new_path != old_path && old_path.exists() {
170                    fs::remove_file(old_path).await?;
171                }
172            }
173
174            fs::write(path, ast.to_string()).await?;
175        }
176    }
177
178    Ok(created_files)
179}
180
181pub async fn process(manifest: Manifest) -> Result<()> {
182    let output_files = private_process(
183        &manifest,
184        &manifest.input(),
185        &manifest.output(),
186        None,
187        false,
188    )
189    .await?;
190    let Some(polyfill) = manifest.polyfill() else {
191        return Ok(());
192    };
193    let polyfill_cache = polyfill.cache().await?;
194    let polyfill_config = polyfill_cache.config();
195
196    // needed additional modifiers: inject_global_value
197    let mut additional_modifiers: Vec<Modifier> = Vec::new();
198    for (key, value) in polyfill_config {
199        let value = if let Some(val) = polyfill.config().get(key) {
200            val
201        } else {
202            value
203        };
204        let mut identifier = DALBIT_GLOBAL_IDENTIFIER_PREFIX.to_string();
205        identifier.push_str(key);
206        let inject_global_value = rules::InjectGlobalValue::boolean(identifier, *value);
207        additional_modifiers.push(Modifier::DarkluaRule(Box::new(inject_global_value)));
208    }
209
210    if let Some(first_output) = output_files.first() {
211        log::debug!("first output found!");
212        let extension = if let Some(extension) = manifest.file_extension() {
213            extension.to_owned()
214        } else {
215            first_output
216                .extension()
217                .ok_or_else(|| anyhow!("Failed to get extension from output file."))?
218                .to_string_lossy()
219                .into_owned()
220        };
221
222        if let Some(module_path) = first_output.parent().map(|parent| {
223            parent
224                .join(polyfill.injection_path())
225                .with_extension(extension)
226        }) {
227            let _ = private_process(
228                &manifest,
229                polyfill_cache.globals_path(),
230                &module_path,
231                Some(&mut additional_modifiers),
232                true,
233            )
234            .await?;
235
236            let mut exports = polyfill_cache.globals_exports().to_owned();
237            for (key, value) in polyfill.globals() {
238                if exports.contains(key) {
239                    if !value {
240                        exports.remove(key);
241                    }
242                } else {
243                    return Err(anyhow!("Invalid global `{}`", key));
244                }
245            }
246
247            log::info!("[injector] exports to be injected: {:?}", exports);
248
249            let injector = Injector::new(
250                module_path,
251                exports,
252                manifest.target_version().to_lua_version(),
253                polyfill_cache.removes().to_owned(),
254            );
255
256            for source_path in &output_files {
257                injector.inject(source_path).await?
258            }
259        }
260    }
261    Ok(())
262}