elit
v3.6.9
Published
Optimized lightweight library for creating DOM elements with reactive state
Maintainers
Readme
Elit
Elit is a TypeScript toolkit for building browser UIs, dev servers, SSR pages, tests, desktop WebView apps, and small file-backed backends from one package.
The package is split by runtime. Browser-facing APIs live in elit and the client subpaths, server APIs live in elit/server, desktop APIs live in elit/desktop, and the CLI is elit.
AI Quick Context
If you are generating or editing code for Elit, follow these rules first:
- Use
elitor the client subpaths for browser UI code. - Use
elit/serverfor HTTP routes, WebSocket endpoints, middleware, dev server, preview server, and server-side shared state. - Use
elit/desktoponly insideelit desktop ...runtime. Those APIs are injected by the native desktop runtime and are not normal browser globals. - Use
elit/buildfor programmatic bundling. - Use
elit/databasefor the VM-backed file database helpers. - Do not use
elit-server. Old docs may mention it, but the current package export iselit/server. - Prefer subpath imports in generated code. They make environment boundaries obvious.
createRouterView(router, options)returns a function. Render it insidereactive(router.currentRoute, () => RouterView()).- Browser-facing code may import local
.tsfiles during development. Elit rewrites those imports for browser output. - Config files can be
elit.config.ts,elit.config.mts,elit.config.js,elit.config.mjs,elit.config.cjs, orelit.config.json. - Environment files are loaded in this order:
.env.{mode}.local,.env.{mode},.env.local,.env. - Only
VITE_variables are injected into client bundles.
Install
Create a new app with the scaffold:
npm create elit@latest my-app
cd my-app
npm install
npm run devOther package managers:
yarn create elit my-app
pnpm create elit my-app
bun create elit my-app
deno run -A npm:create-elit my-appManual install:
npm install elitIf you want desktop mode, install Cargo as well. The native desktop runtime is built with Rust.
Module Map
Use this table as the import map for generated code.
| Import | Use it for | Main exports |
| --- | --- | --- |
| elit | Client-side all-in-one entry | DOM helpers, element factories, state, styles, router, HMR |
| elit/dom | DOM renderer and SSR string rendering | dom, render, renderToString, mount |
| elit/el | HTML, SVG, and MathML element factories | div, button, html, body, script, and many more |
| elit/native | Shared native generation foundation using the same Elit syntax | renderNativeTree, renderNativeJson, renderAndroidCompose, renderSwiftUI |
| elit/state | Reactive state and render helpers | createState, computed, reactive, text, bindValue, bindChecked, createSharedState |
| elit/style | CSS generation and injection | CreateStyle, styles, renderStyle, injectStyle, addClass, addTag |
| elit/router | Client-side routing | createRouter, createRouterView, routerLink |
| elit/server | HTTP router, dev server, middleware, WebSocket endpoints, shared server state | ServerRouter, createDevServer, cors, logger, rateLimit, compress, security, StateManager |
| elit/smtp-server | SMTP listeners and config-friendly wrappers built on top of smtp-server | SMTPServer, createSmtpServer, startSmtpServer |
| elit/build | Programmatic build API | build |
| elit/desktop | Native desktop window APIs | createWindow, createWindowServer, onMessage, windowQuit, windowSetTitle, windowEval |
| elit/database | VM-backed file database | Database, create, read, save, update, rename, remove |
| elit/test | Test runner module entry | test runtime helpers used by the CLI |
Advanced subpaths also exist for lower-level adapters and internals: elit/http, elit/https, elit/ws, elit/wss, elit/fs, elit/path, elit/mime-types, elit/chokidar, elit/runtime, elit/test-runtime, elit/test-reporter, and elit/types.
Native Target Foundation
Elit now includes a practical native-generation foundation that keeps the existing element syntax and can emit a serializable native tree, Jetpack Compose, and SwiftUI from the same source tree. It is still a CSS-subset renderer rather than a browser-complete Android/iOS engine, but it is already useful for shared mobile UI scaffolds, parity checks, and generated native screens.
That same foundation also feeds native desktop mode: Elit resolves one shared native tree and style/layout model, then emits IR, Compose, SwiftUI, or native desktop output from it. Public elit/native APIs stay the same while parity fixes and native CSS-subset improvements can land across outputs together.
On the desktop-native backend, renderer responsibilities are now split internally by concern too: widget rendering, content and media surfaces, form controls, container layout, vector drawing, interaction dispatch, runtime support, and app orchestration no longer live in one monolithic renderer file. That does not change the public API, but it makes parity fixes for buttons, inputs, media surfaces, layout, and vector output safer to land across desktop native, IR, Compose, and SwiftUI outputs without forking the shared native tree contract.
import { a, button, div, h1, img, input } from 'elit/el';
import { renderNativeTree } from 'elit/native';
const screen = div(
{ className: 'screen' },
h1('Hello Native'),
input({ value: 'search', placeholder: 'Search' }),
input({ type: 'checkbox', checked: true }),
a({ href: 'https://elit.dev/docs' }, 'Open docs'),
button({ onClick: () => {} }, 'Tap'),
img({ src: './logo.png', alt: 'Logo' })
);
const nativeTree = renderNativeTree(screen, { platform: 'android' });You can also emit Jetpack Compose code from the same syntax:
import { renderAndroidCompose } from 'elit/native';
const composeFile = renderAndroidCompose(screen, {
packageName: 'com.example.generated',
functionName: 'HomeScreen',
includePreview: true,
});And the same source tree can emit SwiftUI code too:
import { renderSwiftUI } from 'elit/native';
const swiftFile = renderSwiftUI(screen, {
structName: 'HomeScreen',
includePreview: true,
});You can generate these files directly from the CLI too:
npx elit native generate android ./src/native-screen.ts --name HomeScreen --package com.example.app
npx elit native generate ios ./src/native-screen.ts --out ./ios/HomeScreen.swift
npx elit native generate ir ./src/native-screen.ts --platform android --export screenThe entry module can either export a VNode tree, export a zero-argument function that returns one, or call render(...) so the CLI can capture the rendered VNode from a shared entry. By default the CLI auto-detects default, screen, app, view, and root exports before falling back to that captured render path.
For the current native CSS subset, supported style mapping, and parity backlog, see docs/native-css-support.md. For element and factory coverage across elit/el, see docs/native-element-support.md.
Current scope:
- Reuses existing Elit element syntax and VNode output.
- Maps common tags into generic native components such as
View,Text,Button,Image, andTextInput. - Maps checkbox inputs into native toggle controls and turns absolute
hreflinks into native URL-opening or download actions. - Maps a practical control-attribute subset too: disabled button/input/select states, read-only text inputs, initial text-input focus, native keyboard or secure-entry handling for common text input types such as
password,email,number,tel, andurl, practical text-input constraint validation (min/max/step/minLength/maxLength/pattern), link target/rel/download hints, required single-select empty placeholders, and both static and bound-array nativeselect[multiple]checklist flows. - Produces serializable IR suitable for future Android/iOS code generators.
- Can generate Jetpack Compose code for a practical subset of shared mobile UI.
- Can generate SwiftUI code for the same practical subset.
- Can render a practical SVG vector subset in native output, including
circle,rect,path,line,polyline,polygon, andellipseunder supportedsvgroots. - Can parse practical SVG path data beyond straight segments, including
C,Q,S,T, andAcommands, and approximate them into native curve output. - Can render first-pass native WebView and audio/video surfaces when
iframe/object/embed/portaloraudio/videonodes provide a usable source, including practical accessibility labels, mediamutedhandling, and a platform-approximatedcontrols/poster/playsinlinevideo subset, while source-less or unsupported cases stay explicit placeholders. - Can map a practical accessibility subset too: explicit
role,aria-label,aria-description, and selected/checked/disabled/required/expanded/value-text states now feed generated Compose and SwiftUI accessibility output. - Can render a first-pass native
canvassurface with browser-like intrinsic sizing (300x150by default, or explicitwidth/heightattrs when provided), and it now accepts serializable declarativedrawOpsfor shape and path drawing. Browser Canvas 2D and WebGL imperative APIs remain outside the translated surface. - Resolves a practical native CSS subset for typography, spacing, gradients, shadows, flex and simple grid layouts,
currentColor, named colors, and per-side solid, dashed, and dotted borders. - Supports native selector matching for tag, class, id, and attribute selectors; sibling combinators; child and type position pseudo-classes; and practical
:not(...)and:has(...)subsets. - Can be wired into
elit mobile sync|build|runthroughmobile.native.entryso generated files land in the mobile scaffold automatically. - Keeps source tag information so platform backends can apply custom rules later.
Fastest Working App
This is the smallest browser app that works with the current CLI.
Project structure:
my-app/
index.html
src/
main.tssrc/main.ts
import { div, h1, button } from 'elit/el';
import { createState, reactive } from 'elit/state';
import { render } from 'elit/dom';
const count = createState(0);
const app = div(
{ className: 'app' },
h1('Hello Elit'),
reactive(count, (value) =>
button({ onclick: () => count.value++ }, `Count: ${value}`)
)
);
render('app', app);index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Elit App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>Run it:
npx elit devNotes:
render('app', app)andrender('#app', app)both work.- During development you can point the browser to
/src/main.tsdirectly. - For production builds, your copied HTML should point to the built asset path such as
/main.js.
CLI
Main commands:
npx elit dev
npx elit build --entry ./src/main.ts --out-dir dist
npx elit --version
npx elit preview
npx elit test
npx elit desktop ./src/main.ts
npx elit desktop run --mode native
npx elit desktop build ./src/main.ts --release
npx elit mobile init
npx elit mobile run android
npx elit native generate android ./src/native-screen.ts --name HomeScreen
npx elit pm start --script "npm start" --name my-app
npx elit wapk pack .
npx elit wapk run ./app.wapk
npx elit wapk run ./app.wapk --online
npx elit desktop wapk run ./app.wapkUseful flags:
elit --versionelit -velit dev --port 3000 --host 0.0.0.0 --no-openelit build --entry ./src/main.ts --out-dir dist --format esm --sourcemapelit preview --root dist --base-path /appelit test --watchelit test --file ./testing/unit/database.test.tselit test --describe "Database"elit test --it "saves records"elit test --coverage --coverage-reporter text,htmlelit desktop --runtime quickjs|node|bun|denoelit desktop run --mode nativeelit desktop build --platform windows|linux|macos --out-dir distelit desktop build --compiler auto|none|esbuild|tsx|tsupelit mobile init --app-id com.example.app --app-name "Example App" --web-dir dist --icon ./icon.png --permission android.permission.CAMERAelit mobile doctor --cwd .elit mobile doctor --cwd . --jsonelit mobile sync --cwd . --web-dir dist --icon ./icon.png --permission android.permission.CAMERAelit mobile open android|ioselit mobile run android|ios --cwd . --target <device-id> --prod --icon ./icon.png --permission android.permission.CAMERAelit mobile build android|ios --cwd . --prod --icon ./icon.png --permission android.permission.CAMERAelit native generate android ./src/native-screen.ts --name HomeScreen --package com.example.appelit native generate ios ./src/native-screen.ts --out ./ios/HomeScreen.swift --no-previewelit native generate ir ./src/native-screen.ts --platform android --export screenelit pm start --script "npm start" --name my-app --runtime nodeelit pm start --script "npm start" --name my-app --kill-timeout 12000elit pm start --script "npm start" --name my-app --wait-ready --health-url http://127.0.0.1:3000/health --listen-timeout 5000elit pm start --script "npm start" --name my-app --instances 3elit pm start --script "npm start" --name my-app --watch --watch-path src --restart-policy on-failureelit pm start ./src/worker.ts --name worker --runtime bunelit pm start --wapk ./app.wapk --name packaged-appelit pm start --wapk ./app.wapk --name packaged-app --online --online-url http://localhost:4179elit pm start --wapk gdrive://<fileId> --name packaged-appelit pm start --google-drive-file-id <fileId> --google-drive-token-env GOOGLE_DRIVE_ACCESS_TOKEN --name packaged-appelit pm list --jsonelit pm show my-appelit pm describe my-app --jsonelit pm scale my-app 4elit pm saveelit pm resurrectelit pm logs my-app --lines 100elit wapk pack . --password secret-123elit wapk ./app.wapk --runtime node|bun|denoelit wapk run ./app.wapk --password secret-123 --sync-interval 100 --watcherelit wapk run ./app.wapk --online(stays active untilCtrl+C, then closes the shared session)elit wapk gdrive://<fileId> --google-drive-token-env GOOGLE_DRIVE_ACCESS_TOKEN --onlineelit wapk run ./app.wapk --online --online-url http://localhost:4177elit wapk pack .elit wapk patch ./app.wapk --from ./patch.wapkelit wapk inspect ./app.wapk --password secret-123elit wapk extract ./app.wapkelit desktop wapk ./app.wapk --runtime node|bun|deno --watcherelit desktop wapk run ./app.wapk --runtime bun --password secret-123
Desktop mode notes:
- Cargo is required the first time the native runtime is built.
- TypeScript entries are transpiled automatically when needed.
- Set
desktop.modetohybridornative. Projects withdesktop.native.entrydefault tonative; projects without it default tohybrid. - Set
desktop.entrywhen you want hybrid desktop commands to omit the positional entry, and setdesktop.native.entrywhen you want native desktop commands to do the same. - Hybrid desktop mode uses the WebView runtime. Native desktop mode renders Elit native IR in the dedicated native desktop runtime.
elit desktop runis an explicit alias for the run path;elit desktopstill works as the shorthand form.- Desktop build can prebuild the native runtime even without an entry file.
tsxcompiler mode is Node-only and keeps loading the original source tree instead of bundling it.tsxandtsupcompiler modes require those packages to be installed in the project.- Desktop icon support includes
.ico,.png, and.svg. - Desktop build auto-detects
icon.*andfavicon.*in the entry directory, project directory, and siblingpublic/folders.
Mobile mode notes:
- Mobile mode is implemented by elit directly with native project scaffolding.
- Run
elit mobile initonce in your project to create native scaffold folders. - Set mobile defaults in
elit.config.*undermobile(for example:cwd,appId,appName,webDir,mode,icon,permissions). - Android workflow is fully scaffolded: assets are synced to
android/app/src/main/assets/publicand loaded in WebView. - Set
mobile.native.entryto also generateElitGeneratedScreen.ktandElitGeneratedScreen.swiftfrom the same Elit UI source duringsync,run, andbuild. - Set
mobile.modetonativeorhybrid. Projects withmobile.native.entrydefault tonative; projects without it default tohybrid. - Android scaffold now includes a Compose host that switches between generated native UI and the WebView fallback via generated runtime config and
mobile.mode. - Android icon can be set from config or CLI with
.png/.webpand will be applied to launcher resources. - Android permissions can be set from config (
mobile.permissions) or CLI (--permission) and are written intoAndroidManifest.xml. - Set
mobile.android.targetormobile.ios.targetwhen you want a default device/simulator without repeating--targeton every command. - Use
examples/android-native-examplewhen you want an Android-first native mobile smoke test that scaffolds, generates Compose, and builds through Gradle. - Use
examples/universal-app-examplewhen you want one repo that validates web, desktop, and Android mobile workflows together. - Build your web app first, then run
elit mobile syncbeforeopen,run, orbuild. - If
mobile.modeisnativeandmobile.native.entryis configured, sync can still proceed even when the web build output is missing. - Android commands require native tools in your machine (
gradleorgradlew, plusadbforrun). - Use
elit mobile devices android|ios --jsonto inspect connected Android devices or available iOS simulators. - Run
elit mobile doctorto validate local toolchain and project prerequisites before build or run. - Use
elit mobile doctor --jsonwhen you need machine-readable output for CI checks. - iOS scaffold now creates
ios/ElitMobileApp.xcodeprojplus SwiftUI/WebView fallback sources underios/App. - iOS build automation uses
xcodebuildon macOS. - iOS run automation uses
xcrun simctlon macOS and accepts--target booted, a simulator name, or a simulator UDID. Without--target, it prefers a booted simulator and otherwise falls back to the best available iPhone simulator.
WAPK mode notes:
elit wapk <file.wapk>andelit wapk run <file.wapk>run packaged apps.elit desktop wapk <file.wapk>andelit desktop wapk run <file.wapk>run packaged apps in desktop mode.- During run, the archive is expanded into a temporary work directory and changes are synced back to the same
.wapkfile. elit wapk patch <target.wapk> --from <patch.wapk>reads.wapkpatchfrom the patch archive and overlays only the matching archive-relative files into the target archive.- Use
--sync-interval <ms>for polling mode, or--watcher/--use-watcherfor event-driven sync. - Use
--passwordwhen packing, inspecting, extracting, or running a locked archive. - Use
--from-passwordwhen the patch archive is locked with a different password than the target archive. inspectwithout credentials still reports whether the archive is locked, but it does not print the archive contents.- Locked archives stay encrypted when live sync writes changes back into the same
.wapkfile. - Configure package metadata in
elit.config.*underwapk, and usewapk.lockwhen you want password-protected archives by default. - WAPK stays unlocked by default unless
wapk.lock.passwordor--passwordis provided. - See docs/wapk.md for the full archive guide and
examples/wapk-examplefor an end-to-end sample.
PM mode notes:
elit pm start --script "npm start",elit pm start --file ./app.ts, andelit pm start --wapk ./app.wapkall run through the same detached process manager.- WAPK PM targets can also point at
gdrive://<fileId>or usepm.apps[].wapkRun.googleDriveplus forwarded WAPK run flags likeonline,onlineUrl,syncInterval,watcher, andwatchArchive. elit pm startboots every app frompm.apps[], andelit pm start <name>starts one configured app by name.waitReadyuses the configured health check as a startup gate, andlistenTimeoutdecides how long PM can stay instartingbefore it treats startup as failed.instancesstarts a process group likeapi,api:2,api:3, andelit pm scale <name> <count>grows or shrinks that group later.maxMemoryworks withmemoryAction=restart|stop,cronRestartaccepts cron expressions or@every 30sshorthand,expBackoffRestartDelayadds PM2-style unstable restart backoff,expBackoffRestartMaxDelaycaps that delay, andrestartWindowlimits how long restart attempts keep counting towardmaxRestarts.killTimeoutgives each PM app its own grace window before Elit escalates stop or restart to a forceful kill.elit pm reload <name|all>now performs a rolling stop/start across each matched instance in a process group and waits for each replacement to becomeonlinebefore it moves on.pm.apps[].proxyorelit pm start --proxy-port <port>lets PM own the public HTTP port.strategy: 'proxy'is the default, routes to private child ports, supports multi-instance groups, and forwards websocket upgrades.strategy: 'inherit'shares the listener directly with a single-instance Node.js/.mjs/.cjsfile target.- Single-instance apps that bind the public port directly still incur a restart gap on
reload; PM proxy or inherit mode is the current zero-downtime path. elit pm reset <name|all>clears restart counters and saved exit metadata without deleting the process record.elit pm send-signal <signal> <name|all>forwards a signal such asSIGUSR2orTERMto the active managed child process.- On Node.js,
elit/httpandelit/httpsalso acceptlisten({ fd }), andelit pmcan now drive inherited-listener startup for supported single-instance Node file targets throughpm.apps[].proxy.strategy = 'inherit'. - Use
elit pm list,elit pm list --json,elit pm show,elit pm describe --json,elit pm stop,elit pm restart,elit pm reload,elit pm reset,elit pm send-signal,elit pm delete,elit pm save,elit pm resurrect, andelit pm logsto manage long-running processes. - PM-managed WAPK online hosts close their Elit Run shared session when you use
elit pm stop,elit pm restart, orelit pm delete. - Use
--restart-policy always|on-failure|neverplus--min-uptime <ms>when you want tighter restart-loop control. - Use
--watch,--watch-path,--watch-ignore, and--watch-debouncewhen the process should restart after source changes. - PM
--watchand WAPK--watcherare different: the first restarts the managed process, the second changes how the inner WAPK runtime syncs files. - Use
--health-url,--health-grace-period,--health-interval,--health-timeout, and--health-max-failureswhen the process exposes an HTTP health endpoint. elit pm listnow includes livecpu,memory, anduptimecolumns for running processes.elit pm list --jsonandelit pm jlistexpose machine-readable supervisor state plusliveMetrics, whileelit pm show <name>surfaces the full saved process record in a human-readable form.- PM state and logs are stored in
./.elit/pmby default, or inpm.dataDirwhen configured.elit pm savewrites topm.dumpFileor./.elit/pm/dump.json. - TypeScript file targets with runtime
noderequiretsx; use--runtime bunwhen you want zero-config TypeScript execution.
Config File
Elit loads one of these files from the project root:
elit.config.tselit.config.mtselit.config.jselit.config.mjselit.config.cjselit.config.json
The config shape is:
{
dev?: DevServerOptions;
build?: BuildOptions | BuildOptions[];
preview?: PreviewOptions;
test?: TestOptions;
pm?: {
dataDir?: string;
apps?: Array<{
name: string;
script?: string;
file?: string;
wapk?: string;
wapkRun?: {
file?: string;
googleDrive?: {
fileId?: string;
accessToken?: string;
accessTokenEnv?: string;
supportsAllDrives?: boolean;
};
runtime?: 'node' | 'bun' | 'deno';
password?: string;
syncInterval?: number;
useWatcher?: boolean;
watchArchive?: boolean;
archiveSyncInterval?: number;
};
runtime?: 'node' | 'bun' | 'deno';
cwd?: string;
env?: Record<string, string | number | boolean>;
autorestart?: boolean;
restartDelay?: number;
maxRestarts?: number;
password?: string;
}>;
};
mobile?: {
cwd?: string;
appId?: string;
appName?: string;
webDir?: string;
icon?: string;
permissions?: string[];
android?: {
target?: string;
};
ios?: {
target?: string;
};
native?: {
entry?: string;
exportName?: string;
android?: {
packageName?: string;
functionName?: string;
};
ios?: {
structName?: string;
};
};
};
desktop?: {
mode?: 'hybrid' | 'native';
entry?: string;
native?: {
entry?: string;
};
runtime?: 'quickjs' | 'node' | 'bun' | 'deno';
compiler?: 'auto' | 'none' | 'esbuild' | 'tsx' | 'tsup';
release?: boolean;
outDir?: string;
platform?: 'windows' | 'linux' | 'macos';
wapk?: {
runtime?: 'node' | 'bun' | 'deno';
syncInterval?: number;
useWatcher?: boolean;
release?: boolean;
};
};
wapk?: {
name?: string;
version?: string;
runtime?: 'node' | 'bun' | 'deno';
entry?: string;
scripts?: Record<string, string>;
port?: number;
env?: Record<string, string | number | boolean>;
desktop?: Record<string, unknown>;
lock?: {
password?: string;
};
};
}Example:
import { api } from './src/server';
import { documentShell } from './src/document';
export default {
dev: {
port: 3003,
host: '0.0.0.0',
open: false,
logging: true,
clients: [
{
root: '.',
basePath: '',
ssr: () => documentShell,
api,
ws: [
{
path: '/ws',
handler: ({ ws, query }) => {
ws.send(JSON.stringify({ type: 'connected', room: query.room || 'general' }));
ws.on('message', (message) => {
ws.send(message.toString());
});
},
},
],
},
],
},
build: [
{
entry: './src/main.ts',
outDir: './dist',
outFile: 'main.js',
format: 'esm',
sourcemap: true,
copy: [
{ from: './public/index.html', to: './index.html' },
],
},
],
preview: {
root: './dist',
index: './index.html',
port: 4173,
ws: [
{
path: '/ws',
handler: ({ ws }) => {
ws.on('message', (message) => ws.send(message.toString()));
},
},
],
},
test: {
include: ['testing/unit/**/*.test.ts'],
},
mobile: {
cwd: '.',
appId: 'com.elit.app',
appName: 'Elit App',
webDir: 'dist',
mode: 'hybrid',
icon: './icon.png',
permissions: ['android.permission.INTERNET'],
},
desktop: {
mode: 'native',
native: {
entry: './src/main.ts',
},
runtime: 'quickjs',
compiler: 'auto',
release: false,
outDir: 'dist',
platform: 'windows',
wapk: {
runtime: 'bun',
syncInterval: 150,
useWatcher: true,
release: true,
},
},
pm: {
apps: [
{
name: 'api',
script: 'npm start',
runtime: 'node',
},
{
name: 'worker',
file: './src/worker.ts',
runtime: 'bun',
},
{
name: 'archive-app',
wapk: './dist/app.wapk',
runtime: 'node',
},
],
},
wapk: {
name: 'my-app',
version: '1.0.0',
runtime: 'bun',
entry: './src/server.ts',
port: 3000,
env: {
NODE_ENV: 'production',
},
lock: {
password: 'secret-123',
},
},
};Notes:
dev.wsandpreview.wsregister global WebSocket endpoints.clients[].wsregisters client-specific endpoints and prefixes each path with that client'sbasePath.dev.smtpandpreview.smtpstart SMTP listeners alongside the HTTP server.clients[].smtpstarts SMTP listeners for a specific client branch, but SMTP listeners always bind to their ownhostandportinstead of usingbasePath.- The internal Elit HMR and shared-state socket uses
/__elit_ws, so do not reuse that path for custom endpoints.
Important details:
buildmay be a single object or an array. If it is an array, all builds run sequentially.dev.clientsis the most flexible setup when you want SSR, API routes, multiple apps, or per-client proxy rules.previewsupports the same concepts asdev: multiple clients, API routes, proxy rules, workers, SSR, and HTTPS.- Only
VITE_variables are exposed to client code during bundling. desktopconfig provides defaults forelit desktop,elit desktop run,elit desktop build, andelit desktop wapk. Usedesktop.entryfor hybrid defaults,desktop.native.entryfor native defaults, anddesktop.modeto choose which one runs by default.mobileconfig provides defaults forelit mobile init|sync|open|run|build.pmconfig provides defaults forelit pm. Usepm.apps[]for named processes,pm.dataDirfor metadata/log storage,pm.dumpFileforsave/resurrect, and per-app restart/watch/health settings.wapkconfig is loaded fromelit.config.*, then package metadata is used as fallback.wapk.lock.passwordis the config-level default for locked archives. Use--passwordwhen you want to supply unlock credentials at command time instead of writing them into config.wapk runanddesktop wapk runsync runtime file changes back into the same.wapkarchive.
Browser Patterns
Elements and State
import { div, input, button, span } from 'elit/el';
import { createState, computed, reactive, bindValue } from 'elit/state';
import { render } from 'elit/dom';
const name = createState('Elit');
const count = createState(0);
const label = computed([name, count], (currentName, currentCount) => {
return `${currentName}: ${currentCount}`;
});
const app = div(
input({ type: 'text', ...bindValue(name) }),
reactive(label, (value) => span(value)),
button({ onclick: () => count.value++ }, 'Increment')
);
render('app', app);Router
import { div, nav } from 'elit/el';
import { reactive } from 'elit/state';
import { createRouter, createRouterView, routerLink } from 'elit/router';
const routerOptions = {
mode: 'history' as const,
routes: [
{ path: '/', component: () => div('Home') },
{ path: '/about', component: () => div('About') },
{ path: '/post/:id', component: (params: Record<string, string>) => div(`Post ${params.id}`) },
],
notFound: () => div('404'),
};
const router = createRouter(routerOptions);
const RouterView = createRouterView(router, routerOptions);
const app = div(
nav(
routerLink(router, { to: '/' }, 'Home'),
routerLink(router, { to: '/about' }, 'About')
),
reactive(router.currentRoute, () => RouterView())
);Styling
import { CreateStyle } from 'elit/style';
const css = new CreateStyle();
css.addClass('app', {
minHeight: '100vh',
display: 'grid',
placeItems: 'center',
fontFamily: 'system-ui, sans-serif',
});
css.addClass('button', {
padding: '12px 18px',
borderRadius: '12px',
border: '1px solid #222',
background: '#111',
color: '#fff',
cursor: 'pointer',
});
css.addPseudoClass('hover', {
opacity: 0.92,
}, '.button');
css.inject('app-styles');You can also use the shared singleton export:
import styles from 'elit/style';Server Patterns
Server Router
import { ServerRouter, cors, logger } from 'elit/server';
export const api = new ServerRouter();
api.use(cors());
api.use(logger());
api.get('/api/hello', async (ctx) => {
ctx.res.json({ message: 'Hello from Elit' });
});
api.post('/api/echo', async (ctx) => {
ctx.res.json({ body: ctx.body });
});ServerRouter supports both Elit-style handlers and Express-like handlers:
async (ctx) => { ... }async (req, res) => { ... }- middleware with
use(middleware)oruse('/prefix', middleware)
Programmatic Dev Server
import { createDevServer } from 'elit/server';
const server = createDevServer({
port: 3000,
root: '.',
open: false,
logging: true,
});
console.log(server.url);Custom WebSocket Endpoints
Server:
import { createDevServer } from 'elit/server';
const server = createDevServer({
root: '.',
open: false,
ws: [
{
path: '/ws',
handler: ({ ws, query }) => {
ws.send(JSON.stringify({ type: 'connected', room: query.room || 'general' }));
ws.on('message', (message) => {
ws.send(message.toString());
});
},
},
],
});Client:
const socket = new WebSocket(`ws://${location.host}/ws?room=general`);
socket.addEventListener('message', (event) => {
console.log(event.data);
});
socket.send('hello');Notes:
- Use
dev.wsorpreview.wsfor global endpoints. - Use
clients[].wswhen each client should expose its own endpoint under itsbasePath. - Do not use
/__elit_ws; Elit reserves that path for internal HMR and shared-state traffic.
Shared State Between Server and Client
Client:
import { createSharedState } from 'elit/state';
const counter = createSharedState('counter', 0);
counter.value++;Server:
import { createDevServer } from 'elit/server';
const server = createDevServer({ root: '.', open: false });
const counter = server.state.create('counter', { initial: 0 });
counter.value = 10;
counter.update((value) => value + 1);The client createSharedState() connects over WebSocket to the current host unless you pass a custom wsUrl.
SSR Document Shell
For SSR-style setups, export a document shell and return it from dev.clients[].ssr.
import { html, head, body, title, meta, div, script } from 'elit/el';
export const documentShell = html(
head(
title('My Elit App'),
meta({ charset: 'UTF-8' }),
meta({ name: 'viewport', content: 'width=device-width, initial-scale=1.0' })
),
body(
div({ id: 'app' }),
script({ type: 'module', src: '/src/main.ts' })
)
);For production builds, make sure your built HTML points at the production asset path such as /main.js.
Desktop Mode
Desktop mode now has two backends.
- Hybrid mode runs an Elit entry file inside the existing native WebView shell.
- Native mode loads a native entry, materializes Elit native IR, and renders it with the dedicated native desktop runtime.
Hybrid entries can call createWindow(...) directly or finish with a normal render(...) call from a shared UI entry. When Elit desktop hybrid mode sees a shared render-only entry, it captures the rendered VNode and auto-opens a native window from it.
Desktop now follows the same hybrid | native split as mobile:
desktop.mode: 'hybrid'usesdesktop.entrydesktop.mode: 'native'usesdesktop.native.entryand falls back todesktop.entryfor older configs
Use hybrid mode when you want the desktop shell APIs and a browser-style surface. Use native mode when you want the shared Elit tree to render as a real native desktop UI.
If the resolved desktop entry is configured in elit.config.*, you can run npx elit desktop, npx elit desktop run, or npx elit desktop build without passing the entry path again.
Example desktop entry:
import { createWindow, onMessage, windowQuit, windowSetTitle } from 'elit/desktop';
onMessage((message) => {
if (message === 'desktop:ready') {
windowSetTitle('Elit Desktop');
windowQuit();
}
});
createWindow({
title: 'Elit Desktop',
width: 960,
height: 640,
icon: './public/favicon.svg',
html: `<!doctype html>
<html lang="en">
<body>
<main>Hello from Elit Desktop</main>
<script>
window.addEventListener('DOMContentLoaded', () => {
window.ipc.postMessage('desktop:ready');
});
</script>
</body>
</html>`,
});For a runnable minimal file in this repo, see examples/desktop-simple-example.ts.
For a runnable project-style repo, see examples/desktop-typescript-example/.
Run it:
npx elit desktop run --mode native ./src/main.tsBuild a standalone executable:
npx elit desktop build --mode native ./src/main.ts --releaseDesktop notes:
- Runtime choices:
quickjs,node,bun,deno - Transpiler choices:
auto,none,esbuild,tsx,tsup - Mode choices:
hybrid,native tsxis a Node loader mode, not a bundle mode. Useesbuildortsupwhen you want a relocatable output.- Icon input supports
.ico,.png, and.svg - EXE icon embedding and runtime window icon loading both support SVG now
createWindowServer(app, opts)is available when you want to run an HTTP app inside the desktop shell
Database
Elit includes a small VM-backed database helper that stores files under a directory and can execute TypeScript or JavaScript code against them.
import { Database } from 'elit/database';
const db = new Database({
dir: './databases',
language: 'ts',
});
db.create('users', `export const users = [];`);
await db.execute(`
import { users } from '@db/users';
console.log(users.length);
`);Useful methods:
create(dbName, code)read(dbName)save(dbName, code)update(dbName, fnName, code)rename(oldName, newName)remove(dbName, fnName?)
Programmatic Build API
If you want to bundle from code instead of the CLI:
import { build } from 'elit/build';
await build({
entry: './src/main.ts',
outDir: './dist',
outFile: 'main.js',
format: 'esm',
sourcemap: true,
copy: [
{ from: './public/index.html', to: './index.html' },
],
});Testing
CLI test runner examples:
npx elit test
npx elit test --watch
npx elit test --file ./testing/unit/database.test.ts
npx elit test --describe "Database"
npx elit test --it "should save data"
npx elit test --coverage --coverage-reporter text,htmlThe package also exports elit/test, elit/test-runtime, and elit/test-reporter for advanced use, but most users should stay on the CLI.
Changelog
Latest release notes live in CHANGELOG.md.
Highlights in v3.6.5:
- Added
elit pmfor detached background process management of shell commands, file targets, and WAPK apps. - Added
pm.apps[]andpm.dataDirinelit.config.*for config-first process manager workflows. - Added
elit pm save/elit pm resurrect,pm.dumpFile, watch mode, health checks, and restart-policy controls for the process manager. - Added lifecycle commands for managed apps:
list,stop,restart,delete, andlogs.
Good Defaults For Generated Code
When writing new Elit code, these defaults are usually correct:
- Prefer
elit/el,elit/state, andelit/domin new examples. - Keep server code on
elit/serverand browser code on client subpaths. - Use
ServerRouterfor APIs instead of inventing another HTTP layer first. - Use
createStateplusreactivefor UI updates before adding abstractions. - Use
CreateStyleorelit/stylefor injected CSS. - Use
elit.config.tswhen the project needs SSR, APIs, preview customization, workers, or proxy rules. - Use
examples/correct-configas the reference for a minimal full-stack setup.
Repository Guide
If you are working in this repository, these locations matter most:
src/index.ts: root client exportssrc/dom.ts: renderer and SSR string renderingsrc/state.ts: reactive state, bindings, shared state clientsrc/router.ts: client routersrc/style.ts: CSS generatorsrc/server.ts: dev server,ServerRouter, middleware, shared state serversrc/build.ts: bundler APIsrc/desktop.ts: desktop runtime bindings exposed to TypeScriptsrc/desktop-cli.ts:elit desktopimplementationsrc/database.ts: database VM and helpersexamples/correct-config: minimal full-stack referenceexamples/full-db: larger full-stack example with database usageexamples/universal-app-example: one repo covering web, desktop, and Android mobile smoke flowsexamples/android-native-example: Android-first native mobile validation flowexamples/desktop-typescript-example: project-style desktop app withpackage.json,elit.config.ts, and a TypeScript desktop entryexamples/desktop-simple-example.ts: minimal desktop window example with inline HTML + IPC buttonsexamples/desktop-example.ts: desktop smoke test and runtime examplepackages/create-elit: scaffold templates used bynpm create elit@latestdocs/: the documentation site built with Elit itself
Known Boundaries
- Root
elitexport is client-oriented. Non-client features are on subpaths. - Desktop APIs only exist inside the desktop runtime.
- Client env injection is limited to
VITE_variables. - Production HTML copying is explicit. If you copy
index.html, make sure it points at the built JS file, not the dev-only/src/*.tspath.
Next Reads
examples/correct-configfor the cleanest SSR + API setupexamples/universal-app-examplefor a single repo exercising web, desktop, and Android mobile togetherexamples/android-native-examplefor Android-native mobile validationexamples/desktop-typescript-examplefor a small desktop project you can install and run directlyexamples/desktop-simple-example.tsfor the smallest desktop window exampleexamples/desktop-example.tsfor desktop runtime usageUSAGE_EXAMPLES.mdfor more import combinationsdocs/API.mdfor broader API detail
