Script API
Everything a hook script can reach through the roflux global. Together these let a script act like a plugin: read and write files, generate code, build instances, and change the finished tree before it reaches Studio.
New to hook scripts? Start with the script system, which covers what they are and the loading rules. This page is the full reference.
Every function here is typed in the types.d.luau file RoFlux writes into your project, so you get autocomplete and type checking inside scripts/.
Events
Register a callback once, at the top level of a script. It is then called at the right moment during every build.
| Function | Called when | Gets |
|---|---|---|
roflux.onRead(fn) | a source file has been read | the file, return text to replace it |
roflux.onTransfer(fn) | just before source goes to Studio | the file, return text to replace it |
roflux.transpile(ext, fn) | a file with that extension is found | the file, return what it becomes |
roflux.onTree(fn) | the whole tree is built | the root node, change it or return a new one |
roflux.onCompile(fn) | every build finishes | build info |
roflux.onSync(fn) | a change is sent to Studio | sync info |
roflux.onConnect(fn) | the plugin connects and starts a session | the session |
roflux.onEvent(fn) | something fires the event in ServerStorage | the session, then what was fired |
roflux.onGameEvent(fn) | something fires the event in ReplicatedStorage during a playtest | the session, game session, side, then what was fired |
roflux.onLog(fn) | anything is printed during a playtest | the session, game session, side, message and level |
roflux.onGameStart(fn) | a playtest starts | the session and game session |
roflux.onGameEnd(fn) | a playtest stops | the session and game session |
roflux.onAdded(fn) | a file appears | the file |
roflux.onRemoved(fn) | a file is deleted | the file |
roflux.onChanged(fn) | a file is saved | the file |
roflux.on(name, fn) | any of the above, by name | |
roflux.defer(fn) | the next build | nothing |
The file table
| Field | Example for src/Hud.client.luau |
|---|---|
path | "src/Hud.client.luau" |
name | "Hud.client.luau" |
stem | "Hud" |
folder | "src" |
extension | "luau" |
source | the current source, on source hooks and transpilers |
text | the raw file contents, on transpilers |
className | "LocalScript", on source hooks |
Build info from onCompile
{ initial = false, instances = 42, scripts = 17, files = 30, added = 1, updated = 2, removed = 0 }
initial is true for the first build after the server starts or roflux compile runs. The counts say what changed since the last build.
Sync info from onSync
{ added = 1, updated = 0, removed = 0, summary = "+1 ~0 -0", lines = { "+ ReplicatedStorage/Shared/Coins [ModuleScript]" }, clients = 1 }
This only fires while roflux serve is running, and only when a build actually changed something.
Studio events
These let your game and Studio talk to your hook scripts. Use them to copy the console into a file, run a tool from the command bar, collect stats from a playtest, or anything else you can think of. They only work while roflux serve is running and the plugin is connected.
Sessions
Every time the plugin connects, it starts a session with its own ID, a string like "3F2504E0-4F89-11D3-9A0C-0305E82C3301". Every Studio event hands you this ID first, so you can tell connections apart. Disconnecting ends the session.
roflux.onConnect runs as soon as a session starts, with its ID. Connecting again, even from the same Studio, starts a new session and runs it again.
roflux.onConnect(function(session)
roflux.log("studio connected", session)
roflux.fireStudio("welcome", roflux.project().name)
end)
By the time it runs, the RoFlux events are already in ServerStorage, so fireStudio works straight away.
Every playtest also gets a game session ID, made fresh each time you press Play. The server and every client in that playtest share it, so you can group everything from one run together.
Two events, one for each direction
While connected, the plugin makes a BindableEvent named RoFlux, and inside it a second BindableEvent named Received. Each one only carries messages one way:
| Event | You | Your hook scripts |
|---|---|---|
RoFlux | call :Fire(...) on it | get it in onEvent or onGameEvent |
RoFlux.Received | connect to .Event on it | send to it with fireStudio or fireInGame |
your code ── RoFlux:Fire(...) ─────────► your hook scripts
your code ◄── RoFlux.Received.Event ──── your hook scripts
So to send something to your scripts, fire RoFlux. To hear from your scripts, listen to RoFlux.Received. RoFlux never fires RoFlux itself, and never listens to Received, so anything that reaches your scripts came from someone firing RoFlux, and a message can never bounce back and forth.
The pair lives in ServerStorage in edit mode, and in ReplicatedStorage during a playtest.
From Studio: onEvent
While connected, the plugin puts the RoFlux event in ServerStorage. Fire it from the command bar, another plugin or any edit time code:
-- in the Studio command bar
game.ServerStorage.RoFlux:Fire("export", workspace.Map:GetPivot(), 42)
-- scripts/tools.luau
roflux.onEvent(function(session, action, pivot, count)
if action == "export" then
roflux.fs.write("out/pivot.json", roflux.json.encode(pivot, true))
end
end)
To get answers back in Studio, connect to Received, and have your scripts call fireStudio:
-- in the Studio command bar
game.ServerStorage.RoFlux.Received.Event:Connect(function(name, ...)
print("from my scripts", name, ...)
end)
Both events are only there in edit mode. They are never saved into the place, and they are removed as soon as the plugin disconnects.
From a playtest: onGameEvent
When you press Play while connected, the plugin puts the RoFlux event, with its Received child, in ReplicatedStorage. Both server and client code can fire it:
-- any Script or LocalScript
local RoFlux = game.ReplicatedStorage:FindFirstChild("RoFlux")
if RoFlux then
RoFlux:Fire("coinCollected", player.Name, coin.Position)
end
roflux.onGameEvent(function(session, gameSession, context, name, ...)
roflux.log(context, name, ...)
end)
context is "server" or "client", depending on which side fired it. Use FindFirstChild as above, so the same code runs fine when RoFlux is not connected and the event is not there. It only exists in playtests in Studio, never in a live game.
To get messages from your scripts during a playtest, listen to RoFlux.Received. See Sending back.
Console: onLog
During a playtest, everything printed to the output on the server and on the client is sent to onLog:
roflux.onLog(function(session, gameSession, context, message, level)
roflux.fs.append(`logs/{gameSession}.txt`, `[{context}] [{level}] {message}\n`)
end)
level is "output" for print, "warning" for warn, "error" for errors, or "info". This is only for playtests. Edit mode output is not sent.
Playtests: onGameStart and onGameEnd
roflux.onGameStart(function(session, gameSession)
roflux.log("playtest started", gameSession)
end)
roflux.onGameEnd(function(session, gameSession)
roflux.log("playtest ended", gameSession)
end)
Each playtest sends one start and one end, from the server side.
What your scripts get
Whatever you fire on RoFlux reaches your scripts as plain data, in the same order, with any nil gaps kept. Numbers, strings, booleans and tables arrive as they are. Roblox types can not exist outside Roblox, so each one arrives as a table of its values, with a type field saying what it was:
| Fired | Arrives as |
|---|---|
Vector3, Vector2 | { type = "Vector3", X, Y, Z } |
CFrame | X, Y, Z, Position, LookVector, RightVector, UpVector, Orientation in degrees, and all twelve Components |
Color3 | R, G, B from 0 to 1, and Hex |
BrickColor | Name, Number, Color |
UDim, UDim2 | Scale and Offset, or X and Y holding those |
Rect, NumberRange | Min and Max |
NumberSequence, ColorSequence | Keypoints, a list of points |
An enum like Enum.Material.Neon | EnumType, Name, Value |
Font | Family, Weight, Style |
DateTime | UnixTimestampMillis and Iso |
Ray, Region3, TweenInfo, PhysicalProperties | their properties |
An Instance | ClassName, Name, and Path from GetFullName |
| A function, thread, or anything else | { type = "function", value = "function: 0x..." } |
So a CFrame becomes a table you can read, like pivot.Position.Y, but it has no methods. A table that holds itself is cut off with "<cycle>", and NaN or infinity arrive as the text "nan", "inf" or "-inf".
The plugin groups messages and sends them a few times a second, so a script sees them a moment later, always in the order they happened. If a playtest prints thousands of lines at once, anything past a thousand waiting messages is dropped rather than slowing the game down.
Sending back: fireStudio and fireInGame
Scripts can talk the other way too. These two functions fire the Received event, and your code listens to it:
| Function | Listen with |
|---|---|
roflux.fireStudio(...) | ServerStorage.RoFlux.Received.Event, in edit mode |
roflux.fireInGame(...) | ReplicatedStorage.RoFlux.Received.Event, on the server and on every client of the running playtest |
-- scripts/greeter.luau
roflux.onGameStart(function(session, gameSession)
roflux.fireInGame("config", roflux.json("data/tuning.json"))
end)
-- any Script or LocalScript
local RoFlux = game.ReplicatedStorage:FindFirstChild("RoFlux")
if RoFlux then
RoFlux.Received.Event:Connect(function(name, data)
print(name, data)
end)
end
Send basic things: text, numbers, booleans, and tables of those. They arrive in the same order, with any nil gaps kept. Functions can not be sent, and a Roblox type like a Vector3 does not exist on this side, so send its numbers and build it in the game.
Both return true when the message was handed to the server. That only happens while roflux serve is running, so during roflux compile they return false. A message sent while nothing is listening is dropped: fireStudio needs the plugin connected, and fireInGame needs a playtest running. A playtest only gets messages sent after it started, so onGameStart is a good place to send the first one.
Firing Received yourself only reaches your own listeners. It is never sent to your scripts, since only RoFlux goes that way.
While connected, the plugin also keeps two attributes on ServerStorage, RoFluxSession and RoFluxServer. A playtest reads them to know which session it belongs to and where to send things. They are cleared when the plugin disconnects.
Logging
| Function | What it does |
|---|---|
roflux.log(...) | Prints to the server output, tagged hook. |
roflux.warn(...) | Prints as a warning. |
roflux.error(...) | Prints as an error. It does not stop the build. |
roflux.inspect(value) | Turns any value into readable text, with tables laid out and keys sorted. |
Tables passed to log, warn and error are printed through inspect for you, so roflux.log(someTable) shows the contents instead of an address. To actually stop a build, call Luau's own error().
Files
All paths are relative to the project folder. Absolute paths and .. are refused, so a script can never touch anything outside the project.
| Function | Returns |
|---|---|
roflux.fs.read(path) | The contents, or nil if the file is missing. Works for binary files too. |
roflux.fs.write(path, text) | true if it wrote, false if the file already held exactly that. Makes any missing folders. |
roflux.fs.append(path, text) | Nothing. Adds to the end of a file, creating it if needed. |
roflux.fs.remove(path) | true if something was removed. Works on files and empty folders. |
roflux.fs.mkdir(path) | Nothing. Makes the folder and any parents. |
roflux.fs.exists(path) | Whether anything is there. |
roflux.fs.isFile(path) | Whether it is a file. |
roflux.fs.isDirectory(path) | Whether it is a folder. |
roflux.fs.list(path) | Names directly inside a folder, sorted. |
roflux.fs.walk(path) | Every file below a folder, as project paths. |
roflux.fs.glob(pattern) | Every project file matching a pattern like "data/**/*.json". |
roflux.fs.stat(path) | { size, modified, isFile, isDirectory }, or nil. |
roflux.fs.root() | The full path of the project folder. |
In a glob, * matches within one folder and ** matches any number of folders. walk and glob skip hidden folders, target and node_modules.
A file written inside the project is seen by the watcher, which starts a build, which runs your script again. write skips files whose content would not change, so this settles on its own. It only loops forever if you write something different every time, like the current time.
remove will never delete the project folder itself, and it will not delete a folder that still has files in it. The older names roflux.read, roflux.exists, roflux.isDirectory, roflux.list, roflux.walk and roflux.root still work and point at the same functions.
Data formats
| Function | What it does |
|---|---|
roflux.json(path) | Reads and decodes a JSON file. Same as roflux.json.read. |
roflux.json.decode(text) | Text to a table. |
roflux.json.encode(value, pretty?) | A table to text, on one line or laid out. |
roflux.toml.read(path) | Reads and decodes a TOML file. |
roflux.toml.decode(text) | Text to a table. |
roflux.toml.encode(value) | A table to text. |
roflux.base64.encode(text) | Encodes text or binary. |
roflux.base64.decode(text) | Decodes back to the original. |
roflux.hash(text) | A SHA-256 hash, as 64 hex characters. |
Encoders sort object keys, so the same data always produces the same text. That matters when the output becomes a script, because text that shuffles every build would look like a change every build.
Writing Luau
These turn data into Luau source, which is the heart of most generators.
| Function | Example |
|---|---|
roflux.luau.literal(value) | Any value as Luau source, so a table becomes a table you can paste into code |
roflux.luau.module(value) | The same, with return in front, ready to be a ModuleScript |
roflux.luau.quote(text) | say "hi" becomes "say \"hi\"" |
roflux.luau.isIdentifier(name) | Whether a name can be written without quotes |
local levels = {}
for _, file in roflux.fs.glob("data/levels/*.json") do
levels[roflux.path.stem(file)] = roflux.json(file)
end
local source = roflux.luau.module(levels)
Types for files you transpile
A transpiled file is not Luau, so your editor has nothing to read when you require what it became. roflux.meta fixes that. It writes a small Luau module into a MetaStubs folder and points the sourcemap at it, so you get autocomplete and type checking while you keep working in the original file.
| Call | What it writes |
|---|---|
roflux.meta(file, "text") | That Luau source, so you write the types yourself. |
roflux.meta(file, value) | return plus the value, so the types are worked out from the data. |
The first argument is the file table a transpiler is given, or a path like "data/shop.csv".
-- scripts/csv.luau
roflux.transpile("csv", function(file)
local rows = parse(file.text)
roflux.meta(file, rows)
return roflux.luau.module(rows)
end)
Now require(script.Parent.Shop) is typed like the table itself, so shop[1].price is checked and autocompletes. To write the types by hand instead, pass source:
roflux.meta(file, [[
export type Row = { name: string, price: number }
return (nil :: any) :: { Row }
]])
Pass the source you generated straight to roflux.meta. The stub is then the real module, so the types are exactly right with no extra work.
The MetaStubs folder
- A file at
data/shop.csvgets a stub atMetaStubs/data/shop.csv.luau, so two files can never collide. - Every stub starts with a comment naming the file it belongs to.
- Stubs are rewritten whenever the file changes, and removed when the file is deleted or the script stops asking for one. The folder is removed when nothing needs it.
- On startup, a stub whose file is gone is deleted, so a file you removed while RoFlux was closed does not leave one behind.
- The folder is never compiled into your game, and saving into it does not start another build.
- It is generated, so add
MetaStubs/to your.gitignore.
A normal .luau file is already read by your editor. A stub for one would be read instead of the real file, so use roflux.meta for files that are not Luau.
Building instances
roflux.new builds a declaration without writing ["$ClassName"] by hand. It works like Instance.new with the properties filled in.
roflux.new("Frame", {
Name = "Card",
Size = roflux.types.fromScale(0.3, 0.2),
BackgroundColor3 = roflux.types.hex("#12141A"),
roflux.new("TextLabel", {
Name = "Title",
Text = "Play",
FontFace = roflux.types.font("GothamSSm", "Bold"),
}),
})
It sorts each field out for you:
Namebecomes the instance name.- Numbers, text, booleans and arrays are properties.
- Other declarations, including other
roflux.newcalls, are children. - Any other table, like the
FontFaceabove, is moved into$Propertiesso it is set as a property instead of turning into a child by mistake. - Keys starting with
$are kept as they are.
Children can be listed by position, as above, as long as each one has a Name. You can also pass them as a third argument, either as a list or as a table keyed by name.
Return the result from a transpiler, or turn it into a tree node with roflux.tree.node.
Value helpers
Small functions that produce the value shapes described on the Value types page. Everything they return is an array, so it works when syncing and in place files.
| Function | Gives |
|---|---|
roflux.types.hex("#3C91F5") | a Color3 |
roflux.types.rgb(60, 145, 245) | a Color3 from 0 to 255 numbers |
roflux.types.color(0.2, 0.5, 1) | a Color3 from 0 to 1 numbers |
roflux.types.hsv(0.6, 0.8, 1) | a Color3 from hue, saturation and value |
roflux.types.vector3(x, y, z) | a Vector3 |
roflux.types.vector2(x, y) | a Vector2 |
roflux.types.udim(scale, offset) | a UDim |
roflux.types.udim2(xs, xo, ys, yo) | a UDim2 |
roflux.types.fromScale(x, y) | a UDim2, like UDim2.fromScale |
roflux.types.fromOffset(x, y) | a UDim2, like UDim2.fromOffset |
roflux.types.cframe(x, y, z) | a CFrame, or pass all twelve numbers |
roflux.types.rect(minX, minY, maxX, maxY) | a Rect |
roflux.types.range(min, max) | a NumberRange |
roflux.types.numberSequence(...) | a number, or a list of points |
roflux.types.colorSequence(a, b) | a fade from one color to another |
roflux.types.font("GothamSSm", "Bold") | a FontFace, with the family path filled in |
roflux.types.typed(name, value) | the explicit $Type form |
hex, rgb and hsv all return numbers between 0 and 1. That way a very dark color like #010101 can never be mistaken for white, which would happen if it came back as [1, 1, 1].
Text
| Function | Example |
|---|---|
roflux.text.trim(s) | " hi " becomes "hi" |
roflux.text.trimStart(s), trimEnd(s) | trim one side only |
roflux.text.startsWith(s, prefix) | true or false |
roflux.text.endsWith(s, suffix) | true or false |
roflux.text.replace(s, find, with) | plain text, so "." means a dot |
roflux.text.split(s, separator) | a list, split on commas by default |
roflux.text.lines(s) | a list of lines, handling Windows line endings |
roflux.text.indent(s, prefix) | every line indented, with a tab by default |
roflux.text.words(s) | "fooBar_baz" becomes { "foo", "Bar", "baz" } |
roflux.text.pascal(s) | "HelloWorld" |
roflux.text.camel(s) | "helloWorld" |
roflux.text.snake(s) | "hello_world" |
roflux.text.kebab(s) | "hello-world" |
roflux.text.title(s) | "Hello World" |
roflux.text.padStart(s, width, fill) | ("7", 3, "0") becomes "007" |
roflux.text.padEnd(s, width, fill) | the same, on the other side |
Paths
| Function | Example |
|---|---|
roflux.path.join(...) | ("a", "b/../c", "d.txt") becomes "a/c/d.txt" |
roflux.path.normalize(p) | tidies slashes, . and .. |
roflux.path.dirname(p) | "a/b/c.txt" becomes "a/b" |
roflux.path.basename(p) | "a/b/c.txt" becomes "c.txt" |
roflux.path.stem(p) | "a/Hud.client.luau" becomes "Hud.client" |
roflux.path.extension(p) | "a/B.LUAU" becomes "luau" |
roflux.path.withExtension(p, ext) | ("a/b.txt", "md") becomes "a/b.md" |
path.stem only removes the last extension. The stem on a file table removes all of them, so it gives "Hud".
The tree
roflux.onTree hands you the finished tree just before RoFlux writes the sourcemap and syncs to Studio. Anything you change is exactly what Studio receives. This is how a script can tag instances, inject generated ones, rename things, or remove what it does not want.
roflux.onTree(function(root)
roflux.tree.each(root, function(node, path)
if node.className == "ModuleScript" then
table.insert(node.tags, "Module")
end
end)
local generated = roflux.tree.ensure(root, "ReplicatedStorage/Generated")
roflux.tree.add(generated, roflux.tree.node("Built", roflux.new("StringValue", { Value = roflux.version() })))
end)
A node
| Field | Holds |
|---|---|
name | the instance name |
className | the class |
properties | a table of properties, in the same JSON shapes as project files |
attributes | a table of attributes |
tags | a list of tags |
children | a list of child nodes |
ownership | "managed", "passthrough" or "reference" |
filePaths | the files this node came from |
Helpers
| Function | What it does |
|---|---|
roflux.tree.node(name, declaration) | Makes a new node from a class name or any declaration. |
roflux.tree.find(root, "A/B/C") | Finds a node by path, or nil. |
roflux.tree.child(node, name) | Finds a direct child by name. |
roflux.tree.each(root, fn) | Visits every node with its path. Safe to change the tree while visiting. |
roflux.tree.add(parent, node) | Adds a child, replacing any child with the same name. |
roflux.tree.remove(root, "A/B") | Removes a node and returns it. |
roflux.tree.ensure(root, "A/B") | Finds a folder by path, creating any missing ones. |
A managed node owns its children, so RoFlux deletes anything inside it that the tree does not list. ensure creates passthrough folders on purpose, so it never deletes what is already in the place. If you set a service to "managed", everything in that service that your project does not declare will be deleted from Studio.
Removing a managed node from the tree removes it from Studio on the next sync, just like deleting its file would.
Project and settings
| Function | Returns |
|---|---|
roflux.project() | { name, id, root, manifest } |
roflux.config(key?) | Your settings from the project file, or one of them. |
roflux.env(name) | An environment variable, or nil. |
roflux.platform() | "windows", "linux" or "macos". |
roflux.version() | The RoFlux version. |
Give your scripts settings with a Scripts block in the project file. Anything in it can be read with roflux.config:
{
"ProjectID": "MyGame",
"Scripts": {
"obfuscate": { "enabled": true },
"banner": "built by RoFlux"
}
}
local settings = roflux.config("obfuscate") or {}
if settings.enabled then
-- ...
end
Editing the project file reloads every script, so new settings take effect straight away.
Running programs
local result = roflux.exec("stylua", { "--check", "src" })
if not result.ok then
roflux.warn(result.stdout)
end
roflux.exec(program, args, options) runs a program and waits for it. It returns { ok, code, stdout, stderr }. Pass { cwd = "some/folder" } as options to run it inside a project folder.
Hook scripts run the moment someone starts roflux serve. If running programs were always allowed, opening a project you downloaded could run anything on your computer. So exec refuses to work until the project file says so:
"Scripts": { "AllowExec": true }
Only turn this on for projects you trust. A few more things to know:
- The program gets no input, so anything that waits for you to type will just stop waiting.
- It runs during the build, so a slow program makes every save slow.
- On Windows, commands built into the shell need
cmd, as inroflux.exec("cmd", { "/c", "dir" }), and npm is"npm.cmd".
Cache
A place to keep data between builds. It lives as long as the server does, and survives your scripts being reloaded, so it is the right place to remember expensive work.
| Function | What it does |
|---|---|
roflux.cache.get(key) | The stored value, or nil. |
roflux.cache.set(key, value) | Stores a value. Setting nil removes it. |
roflux.cache.has(key) | Whether something is stored. |
roflux.cache.delete(key) | Removes it, and says whether it was there. |
roflux.cache.clear() | Removes everything. |
roflux.cache.keys() | Every key, sorted. |
Values must be plain data: text, numbers, booleans and tables of those. Functions cannot be stored. Everything is gone when the server stops.
Luau built ins
Hook scripts also have Luau's own libraries: string, table, math, utf8, bit32, buffer and os. From os you get os.time(), os.date() and os.clock(), which is all you need for timestamps.
All scripts in scripts/ share one Luau runtime and load in name order. A global set in one script can be read by the others.
Modules and require
Split a big script into pieces with require. Paths are relative to the script doing the requiring:
| Path | Looks in |
|---|---|
require("./util") | the same folder as this script |
require("../util") | the folder above that |
require("../../util") | and so on, one folder up for each ../ |
-- scripts/lib/strings.luau
local strings = {}
function strings.shout(text)
return string.upper(text) .. "!"
end
return strings
-- scripts/banner.luau
local strings = require("./lib/strings")
roflux.onTransfer(function(file)
return "-- " .. strings.shout(file.stem) .. "\n" .. file.source
end)
How a path is found:
"./util"findsutil.luau, thenutil.lua, then a folder namedutilwith aninit.luauinside.- You can write the extension too, as in
"./util.luau". - A path must start with
./or../. A bare name like"util"is refused, so it is always clear where a file comes from. - A path can not climb out of the project folder.
Each file runs once. Every later require of it gets back the same value, even from a different script. Files inside scripts/ are loaded as hook scripts too, but a file that was already required is not run a second time. So a helper module in scripts/lib/ is safe.
Modules can live anywhere in the project, not just in scripts/. RoFlux remembers every file a script required, and saving any of them reloads your scripts, just like saving a file in scripts/ does.
Two files that require each other are stopped with an error instead of looping forever. Your editor follows these paths too, so a required module's functions show up in autocomplete.
Examples
Generate a module from data files
roflux.onTree(function(root)
local levels = {}
for _, file in roflux.fs.glob("data/levels/*.json") do
levels[roflux.path.stem(file)] = roflux.json(file)
end
local shared = roflux.tree.ensure(root, "ReplicatedStorage/Generated")
roflux.tree.add(shared, roflux.tree.node("Levels", roflux.new("ModuleScript", {
Source = roflux.luau.module(levels),
})))
end)
The data folder does not need to be part of the tree at all. Because it is inside the project, saving any level file rebuilds and updates the module in Studio.
Stamp every script in release builds
local release = roflux.env("RELEASE") ~= nil
roflux.onTransfer(function(file)
if not release then
return nil
end
return `-- {roflux.project().name} {os.date("%Y-%m-%d")}\n` .. file.source
end)
Skip work that has not changed
roflux.onTransfer(function(file)
local key = "minified:" .. roflux.hash(file.source)
local done = roflux.cache.get(key)
if done then
return done
end
local result = minify(file.source)
roflux.cache.set(key, result)
return result
end)
Report every sync to a log file
roflux.onSync(function(info)
roflux.fs.append("logs/sync.log", os.date("%H:%M:%S ") .. info.summary .. "\n")
end)
Put the log folder somewhere that is not part of your tree. Writing into the project still wakes the watcher, but nothing in the tree changes, so it will not sync again.