Quickstart
Clone, configure, build, run and deploy — with the commands for each.
From an empty directory to a deployed Worker. Every command here is the moddable
CLI, which resolves the project root by walking up from wherever you run it — so these work
from anywhere in the tree.
Requirements
- Bun 1.2 or newer. Always
bun— nevernpmortsc. - A Cloudflare account, for anything past local development.
Start a project
git clone <provider>/moddable.cli.git moddable/cli
bun moddable/cli/build.ts
moddable config all
moddable build
The CLI build bootstraps rather than assuming. It writes or corrects
tsconfig.json — the paths every moddable/… specifier
resolves through — and index.d.ts, then re-runs itself once, because Bun reads a
tsconfig when it starts and the one just written is invisible to the process that wrote it.
An existing tsconfig is corrected in place: missing aliases are added, anything the project
already set is left alone, and every change is printed.
A CLI-only clone cannot bundle on its own — its source imports
moddable/core and moddable/modules/cloudflare. Rather than a page of
resolver errors, the build names the directories it is missing and prints the
git clone for each, deriving the provider from the CLI's own git remote.
Configure
moddable config is how a project gets set up and how a setup gets checked. Each
target takes generate or validate; a target on its own generates when
it is missing and validates when it is there. With no target at all it walks every target in
order — the one command a fresh clone runs.
moddable config # check everything, generate what is missing
moddable config all # generate every target
moddable config wrangler # just the worker config
moddable config dependencies --install
Targets include typescript, scripts, dependencies,
gitignore, wrangler, settings, server,
types, site, storybook, tests and
repo.
Three of them are composed rather than copied. server, types and
settings are all written from a single scan of the modules on disk, so adding a
module is making a directory: moddable config server then imports it, registers
it, and re-exports its durable objects. Composing is not clobbering — the generators read the
order already in the file and keep it, dropping what is gone and appending what is new.
-y answers every prompt with its default so a whole setup is scriptable,
--force overwrites files that already exist, and --install installs
the packages each setup declares.
Run it locally
moddable dev --watch --protocol=http
moddable config wrangler creates the wrangler.json this needs. That
file holds the account id, the API token and the signing keys, so it is the one entry an
unignored .gitignore reports as a failure rather than a note.
Build
moddable build runs everything. The individual targets are worth knowing when you
are iterating on one of them:
| Command | Output |
|---|---|
moddable server export |
moddable/server/dist/index.js — the Worker |
moddable app export |
moddable/site/app.js — the browser runtime |
moddable views export |
moddable/site/views/<tag>.js |
moddable elements export |
moddable/site/elements/<tag>.js |
moddable assets export |
moddable/site/assets |
Each of the component builds takes a filter — moddable views export --view=<dir>,
moddable assets export --asset=<path> — and skips anything whose checksum has
not changed, so a rebuild after one edit compiles one thing.
Add a page
Make a directory under a module's api/views with four files:
moddable/core/api/views/HelloView/
├── index.ts
├── template.html
├── style.css
└── logic.ts
// index.ts
import { makeView } from 'moddable/core/shared/utils/views/makeView'
import template from './template.html'
import style from './style.css'
import logic from './logic'
export default makeView({
name: 'HelloView',
tag: 'hello-view',
pathname: '/hello',
head: { title: 'Hello' },
template,
style,
logic
})
Register it in the module's views/index.ts, then
moddable views export. The tag must be unique across the project — it is both the
custom element name and the filename the bundle is served under.
Test
moddable tests # every suite, every module
moddable tests core # every suite for one module
moddable tests unit # one suite, every module
moddable tests core worker # one suite for one module
Module and suite share one positional slot because they never collide, so the two read the same
in either order. Vitest owns *.test.ts; Playwright owns *.spec.ts. The
end-to-end suite runs against compiled output, so moddable build comes first.
Never bun test — that is Bun's own runner and it ignores vitest.config.ts.
Deploy
moddable build
moddable deploy
The worker serves the site it was compiled with, so there is nothing else to upload. Next: the Auth API.