lumio build
Build the extension bundle for deployment. Produces optimized client bundles for each surface, the server bundle and functions manifest (with "server": true), and a source archive.
Usage
lumio build
Run it in the project root, next to lumio.config.json. Output always goes to dist/.
What it produces
After a successful build, the dist/ directory contains per-target bundles and an optional assets/ directory for referenced static files:
dist/
├── editor.js <- Editor surface bundle
├── layer.js <- Layer surface bundle
├── styles.css <- Combined CSS (Tailwind or plain)
├── assets/ <- Static assets referenced by components
│ ├── logo-a1b2c3.png
│ ├── font-d4e5f6.woff2
│ └── animation-g7h8i9.lottie
├── server.js <- Server bundle (ESM), only if server: true
├── functions.json <- Functions manifest, only if server: true and server/functions.ts exists
└── source.tar.gz <- Source archive (required by review process)
server.js is built from server/handler.ts or server/index.ts; without either, from server/functions.ts.
Functions manifest
With "server": true, lumio build bundles and evaluates server/functions.ts and writes every named export to dist/functions.json. lumio deploy uploads it with the version, so Lumio can look a function up by the name the client calls (useQuery("getRules")). Automation nodes skip this step - they use their own handler model. Bot modules build it too, so their layer and editor can read what chat and reward handlers wrote to ctx.db (see Viewer input).
{
"version": 1,
"functions": {
"getRules": { "kind": "queryRows", "category": "query", "editorOnly": false, "table": "rules", "orderBy": "created_at", "orderDir": "desc", "limit": { "__expr": "arg", "name": "limit" } },
"addRule": { "kind": "insertRow", "category": "mutation", "editorOnly": true, "table": "rules", "values": { "text": { "__expr": "arg", "name": "text" }, "revealed": false } },
"revealRule": { "kind": "patchRow", "category": "mutation", "editorOnly": true, "table": "rules", "rowId": { "__expr": "arg", "name": "id" }, "values": { "revealed": true } },
"getLeaderboard": { "kind": "handler", "category": "query", "editorOnly": false }
}
}
| Field | Set for | Meaning |
|---|---|---|
kind | all | queryRows, insertRow, patchRow, deleteRow, or handler for query() / mutation() / action() |
category | all | query, mutation or action |
editorOnly | all | true when the definition sets { editorOnly: true } |
table | declarative | Table from server/schema.ts |
values | insertRow, patchRow | Column values; arg() / identity() stay as { "__expr": … } references |
rowId | patchRow, deleteRow | A fixed id or an arg() reference |
filter | queryRows | List of { field, op, value } conditions, all of which must match |
orderBy, orderDir, limit, offset | queryRows | Only when the definition sets them |
Handler entries carry no code: the handler itself ships in server.js.
The build fails, and leaves no functions.json behind, when server/functions.ts:
- exports a value that is not a server function definition (a constant, a helper,
fetchAction()), or a default export - keep helpers in a separate module; - puts a value that is not plain JSON into a definition: a function,
undefined, aDateor other class instance, abigint,NaN/Infinity, or a circular object; - uses
secret()in a declarative function (secrets are only available inaction()handlers), or an unsupportedfilter()operator,orderDir,limit/offsetor row id; - names a table that
server/schema.tsdoes not declare, or declares tables without aserver/schema.ts; - has two exports whose names differ only in case, or a name that is not letters, digits and
_(at most 64 characters).
Error: server/functions.ts: export "getVotes": unknown table "votes" (server/schema.ts declares "rules").
Asset handling
Images, fonts, videos, and other static files imported in your code are automatically copied to assets/ with a content hash in the filename. esbuild handles this via file loaders:
| File type | Output pattern |
|---|---|
.png, .jpg, .gif, .svg, .webp, .avif, .ico | assets/[name]-[hash].[ext] |
.woff, .woff2, .ttf, .otf, .eot | assets/[name]-[hash].[ext] |
.mp4, .webm, .ogg, .mp3, .wav | assets/[name]-[hash].[ext] |
.json, .lottie | assets/[name]-[hash].[ext] |
Import assets in your components and the bundler resolves the correct URL at runtime:
import logoUrl from "./assets/logo.png";
export default function Layer() {
return <img src={logoUrl} alt="Logo" />;
}
Example
lumio build
Building Sports Scoreboard v1.2.0...
Built: dist/server.js
Built: dist/functions.json (4 function(s))
Compiled: dist/styles.css
Built: dist/editor.js
Built: dist/layer.js
Archive: dist/source.tar.gz
Build complete.
CSS and Styling
lumio build supports three styling approaches. You choose during lumio init, but can switch at any time.
Tailwind CSS v4 (recommended)
If src/globals.css contains @import "tailwindcss", the build compiles it via the Tailwind CLI before bundling. Use any Tailwind utility classes in your components:
export default function Layer() {
return (
<div className="flex items-center gap-2 bg-black/80 rounded-lg p-4">
<span className="text-white text-sm font-medium">Hello</span>
</div>
);
}
Requires tailwindcss as a dev dependency (npm add -D tailwindcss@^4.3).
Plain CSS
Create src/globals.css (or src/style.css) with regular CSS. The file is copied to dist/styles.css:
.my-widget {
background: rgba(0, 0, 0, 0.8);
border-radius: 8px;
padding: 16px;
}
Import it in your component: import "./globals.css";
Inline styles
No CSS files needed. Style directly via React style props:
export default function Layer() {
return <div style={{ background: "rgba(0,0,0,0.8)", padding: 16 }}>Hello</div>;
}
Source archive
source.tar.gz contains the full source code of your extension (excluding node_modules and dist). It is uploaded alongside the bundle during lumio deploy and made available to reviewers.
Changelog check
lumio build warns when CHANGELOG.md is missing, but still builds. It does not check for an entry matching lumio.config.json's version. Before deploying, keep a versioned entry with release notes. lumio deploy checks the deployed version: system extensions require a non-empty entry even in CI and with -d; third-party extension deployments follow the CLI's less strict rules.
TypeScript errors
The build fails on TypeScript errors. To check for errors without building:
npx tsc --noEmit
Bundle size limits
| Plan | Max bundle size per surface |
|---|---|
| Free | 10 MB |
| Pro | 50 MB |
If a surface bundle exceeds the limit, the build fails with an error. Optimize with dynamic imports or reduce dependencies.