Each .wowlua file is a module: it exports names, and other files import
them. This page covers export and import, how import paths resolve, the
load order wowlua computes for the TOC, and how modules mix with plain Lua
files and the addon table.
Share a function between files
Write export before a declaration to make it visible to other files:
export function clamp(v: number, lo: number, hi: number): number return math.max(lo, math.min(hi, v))end export function lerp(a: number, b: number, t: number): number return a + (b - a) * clamp(t, 0, 1)endThen import it by name where you need it:
import clamp from "/core/math" print(clamp(5, 0, 1))The imported clamp is a local of src/main.wowlua. It is copied out of the
module once, when the file loads, so a call costs the same as a call to any
local function. This is the Lua the two files compile to:
-- src/core/math.lualocal __wl = select(2, ...).__wowlualocal __wl_exports = __wl.module("core/math")local clampclamp = function(v, lo, hi) return math.max(lo, math.min(hi, v))end__wl_exports.clamp = clamp -- src/main.lualocal __wl = select(2, ...).__wowlualocal clamp = __wl.require("core/math").clamp print(clamp(5, 0, 1))Each module's export table is kept in the addon's private runtime, which
hangs off the addon table that WoW passes to every file. Nothing goes into
_G, so a module of another addon with the same path never clashes with
yours.
What you can export
You can export functions, classes, abstract classes, enums, constants, locals, type aliases and interfaces. Types and interfaces are only for the checker and produce no code.
export const VERSION = 3export type Handler = (msg: string) -> ()export local registry: { [string]: Handler } = {} export function register(name: string, h: Handler) registry[name] = hend export class Cache items: { [string]: number } = {}end export enum Mode { Simple, Advanced } export interface Disposable function dispose()end- Exported names are fixed. Assigning to an exported local after its declaration, or to an imported name, is error E1010. Change the contents of an exported table instead, or export a function that does the change.
export localneeds a value (E0119).- Only declarations can be exported. There is no
export { a, b }list, becauseexport {a}is already a Lua call. - All top-level functions, classes and enums of a module are declared at the top of the file, so two functions can call each other whatever their order in the source. Calling one at load time before its definition has run is error E1037.
- In a module, a top-level
function name()is local to the module, exported or not, so you can't create a global by mistake. Write_G.name = name, or use@global, when you want one. A file with noimportand noexportis a script instead and keeps Lua's meaning: its top-level functions are globals. - A top-level
returnis an error in a module (E1032). - Constants are copied into the files that import them. They stay in the export table too, for plain Lua code that reads it.
Ways to import
import clamp, lerp from "/core/math" -- namedimport Cache as ItemCache from "/registry" -- renamedimport * as log from "wowlua:log" -- the whole moduleimport type Handler from "../registry" -- types only, no code local onShow: Handler = function(msg: string) log.info(msg, clamp(lerp(0, 10, 0.5), 0, 8))end local cache = ItemCache.new()cache.items.opened = 1onShow("window opened")- Imports come first in a file, before any other statement (E1011). Only
directive comments such as
--!strictmay come before them. import * as Mbinds the module's export table. Its exported types are available asM.TypeNamein annotations.import typebrings in types only. It never affects the load order. A class imported this way can be used as a type but not extended (E3008).import _ from "./frames.xml"binds no names. It only makes sure that an XML file, a plain Lua file or a module run for its side effects loads before this file._must be the only name in the statement (E0117).- Importing a name the module doesn't export is error E1012.
Where imports look
| You write | It finds |
|---|---|
"./row", "../theme" |
A file relative to the importing file |
"/core/cache" |
src/core/cache.wowlua, from anywhere in the package (the source root is build.src, default src/) |
"wowlua:events" |
A module of the standard library |
"libfoo" |
The main module of a dependency, src/init.wowlua unless its package.main says otherwise |
"libfoo/ui/list" |
Module ui/list inside the dependency libfoo |
Leave out the extension. wowlua tries, in order, x.wowlua, the declaration
file x.d.wowlua, x.lua and x/init.wowlua. A path that names its
extension, such as "./frames.xml", must match exactly. A module that isn't
found is error E1015.
Paths are case-sensitive on every system. A path whose case doesn't match the file is error E1013: WoW on Windows would load it anyway, but the same addon would fail for players on macOS.
Dependencies are covered in Libraries and dependencies.
Load order, worked out for you
WoW runs an addon's files in the order its TOC lists them. wowlua writes the
TOC and computes that order. It follows your imports, and it also reads which
globals and addon-table fields each file defines and uses, in .wowlua files,
plain .lua files and the Lua inside XML files.
- Only what runs while a file loads counts. Code that runs later, in an event handler, a script, a timer or a slash command, doesn't constrain the order, because every file has loaded by then.
- Two modules may import each other, as long as neither needs the other while it loads.
- When files need each other while they load, the order can't work. That is error E1014, which names each use and where it happens.
- When the order can't be decided, for example because two files define the
same global, you get warning W1033. An
import, or abuild.load-orderentry inwowlua.json, settles it. - Every file under
src/is part of the addon, imported or not. A release build leaves out a module that nothing imports and that only declares things, with warning W6002. Test files (*.test.wowlua) only run inwowlua testand are never shipped.
Here theme reads Window and window reads width, both at the top
level:
error[E1014]: load-time dependency cycle between ui/theme.wowlua, ui/window.wowlua ┌─ src/ui/theme.wowlua:4:22 │ 4 │ export local width = Window.defaultWidth() + PADDING │ ^^^^^^ `ui/theme.wowlua` uses the import `Window` from `ui/window.wowlua` while it loads │ ┌─ src/ui/window.wowlua:9:27 │ 9 │ export local frameWidth = width * 2 │ ----- `ui/window.wowlua` uses the import `width` from `ui/theme.wowlua` while it loads │ = note: ui/theme.wowlua: in top-level code = note: ui/window.wowlua: in top-level code = help: move the use into a function that runs later (an event handler, for example), or split a module error: aborting due to 1 previous error
When you bring an existing addon with a TOC that
works, wowlua init writes its file order into build.load-order, and the
build keeps it. The rules are in
Modules §5.4.
Living alongside plain Lua
You don't have to convert an addon in one go. Plain .lua files keep working
as they are: they still get addonName, ns from ..., and the load order
accounts for what they define. .wowlua files can use the addon table too,
with the usual local addonName, ns = ... at the top.
Give an old Lua file types
Have the Lua file register a module, and describe that module in a declaration file with the same name next to it:
local _, ns = ...ns.__wowlua.define("legacy/util", { round = function(n, d) local m = 10 ^ (d or 0) return math.floor(n * m + 0.5) / m end,})declare module "/legacy/util" function round(n: number, decimals: number?): numberendImporting a name from it gives you the declared types, and makes the Lua file load first:
import round from "/legacy/util" print(round(3.14159, 2))If the Lua file writes globals or fields of ns instead, describe those in a
.d.wowlua file with declare global or declare interface AddonNamespace.
See Types.
Use a module from plain Lua
A plain Lua file of the addon reads a module's exports with
select(2, ...).__wowlua.require("core/math"). The key __wowlua of the
addon table belongs to wowlua's runtime; don't use it for anything else.
Sharing with other addons
Modules are private to the addon they are built into. Two addons never share
a module, even in the same workspace. To offer an API to other addons, put
@global on what they should see:
local scores: { [string]: number } = {} @global("MyAddonAPI")export const api = { getScore = function(name: string): number return scores[name] or 0 end,}local api = {getScore = function(name) return scores[name] or 0end}_G.MyAddonAPI = api__wl_exports.api = apiAnother addon in the same workspace that lists yours in
addon-dependencies is checked against your sources, so it sees
MyAddonAPI with its types. For callbacks between addons, see
Talking to other addons.
Where to go next
- Build info and profiles: imports and code that only exist in some builds.
- Libraries and dependencies: import from other packages.
- Bring your addon: move an existing addon over, file by file.