Custom Modules
NicyRuntime provides a powerful custom require() implementation with caching, aliases, and circular dependency detection.
Basic Usage
math_utils.luau:
local MathUtils = {}
function MathUtils.add(a, b)
return a + b
end
function MathUtils.multiply(a, b)
return a * b
end
return MathUtils
main.luau:
local MathUtils = require("math_utils")
print(MathUtils.add(10, 20)) -- 30
Module Resolution Order
When you call require("module"), NicyRuntime searches for files in this order:
module.luauc(compiled bytecode — fastest)module.luau(Luau source)module.lua(Lua source)module/init.luaucmodule/init.luaumodule/init.lua
Relative Paths
Modules can be required using relative paths:
-- From: /project/src/main.luau
local utils = require("./utils/math_utils") -- /project/src/utils/math_utils.luau
local core = require("../lib/core") -- /project/lib/core.luau
Absolute Paths
local config = require("/etc/myapp/config")
Module Caching
Loaded modules are cached by their resolved file path. Subsequent require() calls for the same module return the cached result without re-loading.
Cache Invalidation
The cache uses file fingerprints based on:
- Modification time (
mtime) — nanosecond precision - File size — in bytes
A cached module is reloaded when either the modification time or the file size changes. This provides fast cache validation without the overhead of computing content hashes.
⚠️ Cache Limitations
False positive scenario: The cache uses mtime + size fingerprinting, NOT content hashing. This means:
- If a file is edited and reverted to different content with the same size
- AND the filesystem preserves the original
mtime(e.g., viatouch -ton Linux, or certain backup/restore tools) - The runtime will serve the stale cached version instead of reloading the file
Example of cache miss:
# File has content "A" at time T1
echo "print('version A')" > module.luau
require("module") -- Loads and caches version A
# File is edited to content "B" (same size), mtime is artificially reset to T1
echo "print('version B')" > module.luau
touch -t 202604150000 module.luau -- Resets mtime to original time
require("module") -- ❌ Still returns cached version A!
When this matters:
- Automated backup/restore tools that preserve timestamps
- Git operations that restore files with original timestamps
- Manual
touchcommands that reset modification times - File synchronization tools (rsync, robocopy) with timestamp preservation
When this is NOT a problem:
- Normal development workflow (editing files changes mtime)
- Build pipelines that copy/regenerate files (mtime changes)
- Production deployments (files are static)
Workarounds
If you need robust cache invalidation:
- Manual cache clear: Restart the runtime (clears all cached modules)
- Future feature:
robust-cachefeature flag using SHA-256 content hashing (planned) - Development workflow: Always save files normally (don’t manipulate timestamps)
Cache Scope
- Cache is per-runtime-instance. Each
nicy_start()call creates a fresh cache. - Cache is not shared between separate runtime invocations.
- Cache entries are automatically cleaned up when the runtime shuts down.
Circular Dependencies
NicyRuntime detects circular dependencies and throws a clear error:
-- a.luau
local B = require("b") -- Error: Cyclic require detected: a -> b -> a
Error output:
Error: Cyclic require detected
require chain:
a.luau
→ b.luau
→ a.luau (circular)
Concurrent Loading
If a module is already being loaded by another coroutine, require() will yield and wait for the loading to complete. This prevents duplicate loading of the same module.
The @self Alias
@self refers to the directory of the current script:
-- From: /project/src/main.luau
local utils = require("@self/utils/math") -- /project/src/utils/math.luau
This is especially useful in libraries where you want to require sibling modules without knowing the absolute path.