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.

warning

CHANGELOG.md must include an entry for the version being built. If no entry is found for the current version in lumio.config.json, the CLI prints a warning - the build still succeeds, but release notes will be empty unless you fill them in via the submission wizard.

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.