Skip to content

Commit

Permalink
Add support for .luaurc file (#246)
Browse files Browse the repository at this point in the history
Load require aliases from the nearest `.luaurc` file for 
each processed file.

---------

Co-authored-by: jeparlefrancais <[email protected]>
  • Loading branch information
Stefanuk12 and jeparlefrancais authored Jan 20, 2025
1 parent a6a8f33 commit eebf917
Show file tree
Hide file tree
Showing 11 changed files with 223 additions and 19 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Changelog

* read Luau configuration files (`.luaurc`) to get path aliases ([#246](https://github.com/seaofvoices/darklua/pull/246))
* support Luau types when bundling ([#249](https://github.com/seaofvoices/darklua/pull/249))

## 0.15.0
Expand Down
3 changes: 3 additions & 0 deletions site/content/docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ Any missing field will be replaced with its default value.
// map `pkg` to a path to the `Packages` folder created by Wally:
pkg: "./Packages",
},

// Enable or disable finding `.luaurc` files to load source aliases
use_luau_configuration: true, // default value
},
},

Expand Down
15 changes: 13 additions & 2 deletions site/content/docs/path-require-mode/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ The path require mode can be defined as the string 'path' to use all the default
sources: {
pkg: "./Packages",
},

// optional (defaults to true)
use_luau_configuration: true,
}
```

Expand Down Expand Up @@ -110,7 +113,7 @@ Given this configuration file for bundling:
require_mode: {
name: "path",
sources: {
pkg: "./Packages",
@pkg: "./Packages",
// you can also map directly to a file (Lua or
// any supported data file)
images: "./assets/image-links.json",
Expand All @@ -123,6 +126,14 @@ Given this configuration file for bundling:
It is possible to make these require call in any file:

```lua
local Promise = require("pkg/Promise")
local Promise = require("@pkg/Promise")
local images = require("images")
```

## Luau Configuration Files

Luau configuration files are named `.luaurc` and they can contain an `aliases` parameter which acts like the [sources](#sources) parameter in darklua.

The value of `use_luau_configuration` will change how darklua finds new sources. Before looking at the [sources](#sources) value, darklua will attempt to find the nearest `.luaurc` configuration file to each file it processes. If it finds one, it will load the aliases.

This behavior is enabled by default. It can be disabled by setting `use_luau_configuration` to `false`.
8 changes: 7 additions & 1 deletion src/frontend/worker_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ use std::{
use petgraph::{algo::toposort, graph::NodeIndex, stable_graph::StableDiGraph, visit::Dfs};
use xxhash_rust::xxh3::xxh3_64;

use crate::{frontend::utils::maybe_plural, utils::Timer, DarkluaError};
use crate::{
frontend::utils::maybe_plural,
utils::{clear_luau_configuration_cache, Timer},
DarkluaError,
};

use super::{
normalize_path, work_item::WorkStatus, Configuration, DarkluaResult, Options, Resources,
Expand Down Expand Up @@ -83,6 +87,8 @@ impl WorkerTree {
}

pub fn process(&mut self, resources: &Resources, mut options: Options) -> DarkluaResult<()> {
clear_luau_configuration_cache();

if !self.remove_files.is_empty() {
let remove_count = self.remove_files.len();
log::debug!(
Expand Down
6 changes: 5 additions & 1 deletion src/rules/bundle/require_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ impl BundleRequireMode {
) -> RuleProcessResult {
match self {
Self::Path(path_require_mode) => {
path_require_mode::process_block(block, context, options, path_require_mode)
let mut require_mode = path_require_mode.clone();
require_mode
.initialize(context)
.map_err(|err| err.to_string())?;
path_require_mode::process_block(block, context, options, &require_mode)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/rules/convert_require/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ impl RequireMode {
fn initialize(&mut self, context: &Context) -> DarkluaResult<()> {
match self {
RequireMode::Roblox(roblox_mode) => roblox_mode.initialize(context),
RequireMode::Path(_) => Ok(()),
RequireMode::Path(path_mode) => path_mode.initialize(context),
}
}
}
Expand Down
19 changes: 9 additions & 10 deletions src/rules/require/path_locator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,15 @@ impl<'a, 'b, 'c> RequirePathLocator<'a, 'b, 'c> {
))
})?;

let mut extra_module_location = self.extra_module_relative_location.join(
self.path_require_mode
.get_source(source_name)
.ok_or_else(|| {
DarkluaError::invalid_resource_path(
path.display().to_string(),
format!("unknown source name `{}`", source_name),
)
})?,
);
let mut extra_module_location = self
.path_require_mode
.get_source(source_name, self.extra_module_relative_location)
.ok_or_else(|| {
DarkluaError::invalid_resource_path(
path.display().to_string(),
format!("unknown source name `{}`", source_name),
)
})?;
extra_module_location.extend(components);
path = extra_module_location;
}
Expand Down
41 changes: 39 additions & 2 deletions src/rules/require/path_require_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::frontend::DarkluaResult;
use crate::nodes::FunctionCall;
use crate::rules::require::match_path_require_call;
use crate::rules::Context;
use crate::utils::find_luau_configuration;
use crate::DarkluaError;

use std::collections::HashMap;
Expand All @@ -22,13 +23,23 @@ pub struct PathRequireMode {
module_folder_name: String,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
sources: HashMap<String, PathBuf>,
#[serde(default = "default_use_luau_configuration")]
use_luau_configuration: bool,
#[serde(skip)]
luau_rc_aliases: Option<HashMap<String, PathBuf>>,
}

fn default_use_luau_configuration() -> bool {
true
}

impl Default for PathRequireMode {
fn default() -> Self {
Self {
module_folder_name: get_default_module_folder_name(),
sources: Default::default(),
use_luau_configuration: default_use_luau_configuration(),
luau_rc_aliases: Default::default(),
}
}
}
Expand All @@ -49,15 +60,41 @@ impl PathRequireMode {
Self {
module_folder_name: module_folder_name.into(),
sources: Default::default(),
use_luau_configuration: default_use_luau_configuration(),
luau_rc_aliases: Default::default(),
}
}

pub(crate) fn initialize(&mut self, context: &Context) -> Result<(), DarkluaError> {
if !self.use_luau_configuration {
self.luau_rc_aliases.take();
return Ok(());
}

if let Some(config) = find_luau_configuration(context.current_path(), context.resources())?
{
self.luau_rc_aliases.replace(config.aliases);
} else {
self.luau_rc_aliases.take();
}

Ok(())
}

pub(crate) fn module_folder_name(&self) -> &str {
&self.module_folder_name
}

pub(crate) fn get_source(&self, name: &str) -> Option<&Path> {
self.sources.get(name).map(PathBuf::as_path)
pub(crate) fn get_source(&self, name: &str, rel: &Path) -> Option<PathBuf> {
self.sources
.get(name)
.map(|alias| rel.join(alias))
.or_else(|| {
self.luau_rc_aliases
.as_ref()
.and_then(|aliases| aliases.get(name))
.map(ToOwned::to_owned)
})
}

pub(crate) fn find_require(
Expand Down
93 changes: 93 additions & 0 deletions src/utils/luau_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use std::{
cell::RefCell,
collections::HashMap,
path::{Path, PathBuf},
};

use serde::Deserialize;

use crate::{DarkluaError, Resources};

const LUAU_RC_FILE_NAME: &str = ".luaurc";

#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
pub(crate) struct LuauConfiguration {
#[serde(default)]
pub(crate) aliases: HashMap<String, PathBuf>,
}

fn find_luau_configuration_private(
luau_file: &Path,
resources: &Resources,
) -> Result<Option<LuauConfiguration>, DarkluaError> {
log::debug!(
"find closest {} for '{}'",
LUAU_RC_FILE_NAME,
luau_file.display()
);

for ancestor in luau_file.ancestors() {
let config_path = ancestor.join(LUAU_RC_FILE_NAME);

if resources.exists(&config_path)? {
let config = resources.get(&config_path)?;

return serde_json::from_str(&config)
.map(|mut config: LuauConfiguration| {
log::debug!("found luau configuration at '{}'", config_path.display());

config.aliases = config
.aliases
.into_iter()
.map(|(mut key, value)| {
key.insert(0, '@');
(key, ancestor.join(value))
})
.collect();

Some(config)
})
.map_err(Into::into);
}
}

Ok(None)
}

thread_local! {
static LUAU_RC_CACHE: RefCell<HashMap<Option<PathBuf>, Option<LuauConfiguration>>> = RefCell::new(HashMap::new());
}

pub(crate) fn find_luau_configuration(
luau_file: &Path,
resources: &Resources,
) -> Result<Option<LuauConfiguration>, DarkluaError> {
let key = luau_file.parent().map(Path::to_path_buf);

LUAU_RC_CACHE.with(|luau_rc_cache| {
{
let cache = luau_rc_cache.borrow();

let res = cache.get(&key);
if let Some(res) = res {
return Ok(res.clone());
}
}

let mut cache = luau_rc_cache.borrow_mut();

let value = find_luau_configuration_private(luau_file, resources)?;

cache.insert(key, value.clone());

Ok(value)
})
}

pub fn clear_luau_configuration_cache() {
LUAU_RC_CACHE.with(|luau_rc_cache| {
let mut cache = luau_rc_cache.borrow_mut();
cache.clear();
log::debug!("luau configuration cache cleared");
})
}
5 changes: 3 additions & 2 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
mod expressions_as_statement;
pub(crate) mod lines;
mod luau_config;
mod scoped_hash_map;
mod serde_string_or_struct;
mod timer;

pub(crate) use expressions_as_statement::{expressions_as_expression, expressions_as_statement};
pub(crate) use luau_config::{clear_luau_configuration_cache, find_luau_configuration};
pub(crate) use scoped_hash_map::ScopedHashMap;
pub(crate) use serde_string_or_struct::string_or_struct;
pub use timer::Timer;

use std::{
ffi::OsStr,
iter::FromIterator,
path::{Component, Path, PathBuf},
};
pub use timer::Timer;

use crate::DarkluaError;

Expand Down
49 changes: 49 additions & 0 deletions tests/rule_tests/convert_require.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,55 @@ fn convert_parent_init_module_from_init_module() {
);
}

mod luaurc {
use super::*;

#[test]
fn convert_alias_module_from_init_module() {
let resources = memory_resources!(
"src/init.lua" => "local value = require('@value')",
"src/value.lua" => "return nil",
".luaurc" => r#"{ "aliases": { "value": "src/value.lua" } }"#,
".darklua.json" => CONVERT_PATH_TO_ROBLOX_DEFAULT_CONFIG,
);
expect_file_process(
&resources,
"src/init.lua",
"local value = require(script:FindFirstChild('value'))",
);
}

#[test]
fn convert_folder_alias_module_from_init_module() {
let resources = memory_resources!(
"src/init.lua" => "local value = require('@value/default.lua')",
"src/value/default.lua" => "return nil",
".luaurc" => r#"{ "aliases": { "value": "src/value" } }"#,
".darklua.json" => CONVERT_PATH_TO_ROBLOX_DEFAULT_CONFIG,
);
expect_file_process(
&resources,
"src/init.lua",
"local value = require(script:FindFirstChild('value'):FindFirstChild('default'))",
);
}

#[test]
fn convert_folder_alias_module_from_init_module_without_extension() {
let resources = memory_resources!(
"src/init.lua" => "local value = require('@value/default')",
"src/value/default.lua" => "return nil",
".luaurc" => r#"{ "aliases": { "value": "src/value" } }"#,
".darklua.json" => CONVERT_PATH_TO_ROBLOX_DEFAULT_CONFIG,
);
expect_file_process(
&resources,
"src/init.lua",
"local value = require(script:FindFirstChild('value'):FindFirstChild('default'))",
);
}
}

mod sourcemap {
use super::*;

Expand Down

0 comments on commit eebf917

Please sign in to comment.