Skip to main content

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 }
}
}
FieldSet forMeaning
kindallqueryRows, insertRow, patchRow, deleteRow, or handler for query() / mutation() / action()
categoryallquery, mutation or action
editorOnlyalltrue when the definition sets { editorOnly: true }
tabledeclarativeTable from server/schema.ts
valuesinsertRow, patchRowColumn values; arg() / identity() stay as { "__expr": … } references
rowIdpatchRow, deleteRowA fixed id or an arg() reference
filterqueryRowsList of { field, op, value } conditions, all of which must match
orderBy, orderDir, limit, offsetqueryRowsOnly 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, a Date or other class instance, a bigint, NaN / Infinity, or a circular object;
  • uses secret() in a declarative function (secrets are only available in action() handlers), or an unsupported filter() operator, orderDir, limit / offset or row id;
  • names a table that server/schema.ts does not declare, or declares tables without a server/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 typeOutput pattern
.png, .jpg, .gif, .svg, .webp, .avif, .icoassets/[name]-[hash].[ext]
.woff, .woff2, .ttf, .otf, .eotassets/[name]-[hash].[ext]
.mp4, .webm, .ogg, .mp3, .wavassets/[name]-[hash].[ext]
.json, .lottieassets/[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.

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​

PlanMax bundle size per surface
Free10 MB
Pro50 MB

If a surface bundle exceeds the limit, the build fails with an error. Optimize with dynamic imports or reduce dependencies.