Modules
At this point you know how the most important standard libraries in Lune work and how to use them - and your code may be getting longer and more difficult to read.
Modularizing your code and splitting it across several files in Lune is different from other versions of Lua, and more similar to how things work in other languages such as JavaScript.
File Structure
Section titled “File Structure”Let’s start with a typical module setup that we’ll use throughout this chapter:
- main.luau
- sibling.luau
Directorydirectory
- init.luau
- child.luau
This structure shows the two main patterns you’ll use - individual module files (sibling.luau
) and directory modules (modules/
with its init.luau
).
The contents of these files are not very important for this article, but here is an example for the sake of completeness:
local sibling = require("./sibling")local directory = require("./directory")
print(sibling.Hello) --> World
print(directory.Child.Foo) --> Barprint(directory.Child.Fizz) --> Buzz
print(directory.Sibling.Hello) --> World
return { Hello = "World",}
return { Child = require("@self/child"), Sibling = require("../sibling"),}
return { Foo = "Bar", Fizz = "Buzz",}
How Does It Work?
Section titled “How Does It Work?”Looking at our main file, you’ll notice the require paths always start with ./
or ../
.
This means “relative to the current file”, the same way it does in your terminal.
When main.luau
requires "./sibling"
, Lune looks for sibling.luau
in the same directory as main
.
The interesting part is require("./modules")
.
Lune sees this is a directory and automatically looks for modules/init.luau
.
Inside that init file, we use two different types of require statements:
The statement require("@self/child")
uses the special @self
alias.
Since init files represent their parent directory, @self
here means - inside the “modules” directory.
Without it, require("./child")
would look for child.luau
next to the “modules” directory, not inside it.
Coming from Other Languages
Section titled “Coming from Other Languages”If you’re arriving at Lune with experience in other runtimes & languages, these comparisons may help you get oriented. If you want to get right into the nitty-gritty details, feel free to skip this section completely.
Traditional Lua Structure:
- main.lua
- mylib.lua
Directoryutils/
- init.lua
- helper.lua
-- Lua 5.x - requires are relative to the working directory-- You need to configure package.path:package.path = package.path .. ";./utils/?.lua"
local mylib = require("mylib") -- Only works if CWD is correctlocal utils = require("utils") -- Needs package.path setuplocal helper = require("utils.helper") -- Uses dots, not slashes
-- Lune - requires are relative to the filelocal mylib = require("./mylib") -- Always workslocal utils = require("./utils") -- No path config neededlocal helper = require("./utils/helper") -- Uses slashes like paths
The main difference here is that, in traditional Lua, requires depend on where you run the script from. In Lune, requires are relative to the file containing them, making your code portable and predictable.
JavaScript / TypeScript Structure:
- package.json
- index.js
Directorylib/
- index.js
- helper.js
Directorynode_modules/
Directoryexpress/
- …
Equivalent in Lune:
- main.luau
Directorylib/
- init.luau
- helper.luau
// JavaScriptconst express = require('express') // From node_modulesconst lib = require('./lib') // Local fileconst helper = require('./lib/helper') // Specific file
-- Lune has no centralized package management, yet...local lib = require("./lib") -- Same patternlocal helper = require("./lib/helper") -- Same pattern
File-relative requires are familiar and work the same way.
The difference here is package management and dependency resolution.
JavaScript has standardized on node_modules
for package management,
and there is no standardized package management solution in Lune yet.
Python Structure:
- main.py
Directorymypackage/
__init__.py
- module.py
Directorysubpackage/
__init__.py
- helper.py
Equivalent in Lune:
- main.luau
Directorymypackage/
- init.luau
- module.luau
Directorysubpackage/
- init.luau
- helper.luau
# Python - many ways to import modulesimport mypackagefrom mypackage import modulefrom mypackage.subpackage import helperimport mypackage.module as mod
-- Lune - one single way to import moduleslocal mypackage = require("./mypackage")local module = require("./mypackage/module")local helper = require("./mypackage/subpackage/helper")local mod = require("./mypackage/module") -- Aliasing via assignment
Rust Structure:
- Cargo.toml
Directorysrc/
- main.rs
- lib.rs
Directoryutils/
- mod.rs
- helper.rs
Equivalent in Lune:
- main.luau
- lib.luau
Directoryutils/
- init.luau
- helper.luau
mod lib;mod utils;
use crate::utils::helper;use lib::something;
local lib = require("./lib")local utils = require("./utils")
-- No use statements - access through the module using simple dot notationlocal result = utils.helper.doSomething()local thing = lib.something
Like Rust, init.luau
is your mod.rs
.
Unlike Rust, there’s no visibility modifiers or explicit module declarations - if you return a value, it is always public.
Module Caching
Section titled “Module Caching”Every module you require gets cached on the first call to the require
function.
This means that it is safe to store state within modules, and expose it using public functions:
local count = 0return { increment = function() count += 1 return count end}
local counter1 = require("./counter")local counter2 = require("./counter")
print(counter1.increment()) --> 1print(counter2.increment()) --> 2 (same table & function pointer!)print(counter1 == counter2) --> true
This caching behavior is usually what you want - it prevents duplicate initialization and lets modules maintain internal state. Just remember that if you need separate instances of a class or something similar, you’ll need to return a function that creates its own, separate state.
Extra: Async Caching
Lune actually has an extra trick up its sleeve - it caches modules properly even if they call asynchronous functions during initialization!
This lends itself to some very useful patterns - such as reading configuration files using the asynchronous @lune/fs
standard library during require
.
You can have a single module that handles reading configuration files, and require it concurrently from multiple files, without worrying about race conditions or the configuration being read more than once.
Path Resolution
Section titled “Path Resolution”Lune keeps path resolution simple and predictable.
Paths are case-sensitive on all platforms (even Windows) and always use forward slashes.
When you require "./myModule"
, Lune checks for:
myModule.luau
(preferred extension)myModule.lua
(for compatibility)myModule/init.luau
(directory module)myModule/init.lua
(directory module, compatibility)
The search behavior is also consistent across all platforms.
Configuring Aliases Using .luaurc
Section titled “Configuring Aliases Using .luaurc”Lune supports standardized Luau configuration files that can define aliases and other settings for your project.
To use aliases, you will need to create a JSON-like configuration file named .luaurc
inside of a directory, as such:
{ "aliases": { "utils": "./src/utilities", "config": "./configuration" }}
With these aliases defined, you can use them anywhere in your project, using the @
prefix:
-- Instead of long relative paths ...local config = require("../../../configuration/settings")local helper = require("../../src/utilities/helper")
-- ...you can use aliases!local config = require("@config/settings")local helper = require("@utils/helper")
It is also possible to create multiple .luaurc
configuration files in your project.
When Lune looks for a .luaurc
file, it searches from your script’s directory up through parent directories.
This means you can have project-wide configuration at the root, and override specific settings in subdirectories if necessary.
What’s Next?
Section titled “What’s Next?”You now have all the tools to organize your Lune scripts into clean, reusable modules. You can split code into files, create module hierarchies with directories, and you understand how Lune’s caching mechanism and path resolution work.
But, what happens when you need functionality that Lune doesn’t provide? Sometimes the best solution isn’t to rewrite something in Luau - it’s to use existing tools on your system. Let’s extend Lune’s capabilities by Spawning Programs next.