esdev create
esdev create my-app cd my-app npm install # the manager chosen at scaffolding — npm unless said otherwise npm run dev
What you get is a project with its esdev.json written, its entry named by the script tag in its index.html, its runtime: types installed and wired into tsconfig.json, and — for a project that runs a server — a deploy line that is already narrow:
esrun --allow-read=./dist --allow-env=PORT --allow-listen=8080 dist/server.js
A project that deploys to a static host has no such line, because it runs nothing. Which of the two you get is the mode.
Templates
esdev create --list esdev create my-app --template=react --mode=fullstack
react | React — a static site, or an app with a server of its own |
api | A JSON API under a narrow grant. Nothing it ships depends on |
hono | An HTTP app on Hono — routes, validation and pages behind one server |
elysia | An HTTP app on Elysia — typed routes and validation behind one server |
vanilla | TypeScript and the DOM — no framework |
micro-ui | Micro apps with Micro-UI — framework-free UI with a tiny reactive core |
preact | Preact on the DOM — React's API in three kilobytes |
lit | Lit web components — shadow DOM, client-rendered |
lib | A publishable package — .d.ts and exports wired |
spa | An OTF Web single-page app — browser-only UI, static deploy, no server |
fullstack | An OTF Web fullstack app — loaders, API routes, SSR on demand |
docs | An OTF Web documentation site — MDX docs, sidebar, search, optional blog |
library | An OTF Web component library — publishable components, esdev-tested |
Template files are baked into esdev. During scaffolding, create looks up each dependency's current latest version on the npm registry and writes that exact version to package.json. It prints each resolved version. These lookups need registry access, even when you pass --no-install.
If a lookup fails, esdev retries it three times with increasing delays. If the registry is still unavailable, it reports the package and stops before writing the project.
react, vanilla, micro-ui, preact and lit start with a plain src/styles.css, linked from index.html and bundled by esdev — the page shows the CSS pipeline with nothing to install. --styling=tailwind replaces that stylesheet with one importing Tailwind and adds tailwindcss to devDependencies; esdev compiles it (Tailwind CSS).
esdev create my-app --template=vanilla --styling=tailwind
Styling has no default. On a terminal the question has to be answered; an unattended run without --styling adds nothing to the template.
The OTF Web starters build with the OTF toolchain (otfw, itself an ES-runtime program) rather than esdev build, so they carry no esdev.json — their dev/build scripts call otfw, which runs on the esdev beside it. They take three extra axes, each with a flag, asked where the template takes them:
esdev create my-app --template=spa --language=ts --styling=tailwind esdev create my-app --template=docs --no-blog
--language=js|ts | spa, fullstack, docs, library — JavaScript (default) or TypeScript |
--styling=css|tailwind | spa, fullstack — plain CSS or Tailwind v4 |
--blog, --no-blog | docs — keep the demo blog (default) or withhold it |
TypeScript is a conversion, not a second template: sources rename (.jsx to .tsx) and a tsconfig.json plus the compiler-macro declarations arrive beside them — with the typescript dependency and a typecheck script, so the config has something to act on it. The library's test runs under esdev test in either language.
Every template is a hello world
Each esdev-built template writes the smallest project that runs, builds and tests: a hello(name) function, a test for it, and an entry that shows the greeting. For api, the server answers {"message":"Hello, world!"}.
src/hello.ts hello(name) → "Hello, world!" src/hello.test.ts its test, with runtime:test src/main.ts shows it (App.tsx and entry.client.tsx for react)
The OTF starters are pages rather than packages: one page — the file to edit named in it — and the scripts to run it. Every template's scripts are there from the start: applications have dev and build; lib has build, test and typecheck, without a dev script. The OTF library publishes its source and has test, plus typecheck when TypeScript is selected. Templates add test:dom where they have a DOM test and preview where they build a static site. Nothing chooses a license for you; add one when you publish.
Modes
Some templates are two projects wearing one name. react is: an app with a server of its own is not the same project as a site that deploys to a static host — different files, a different esdev.json, and in one case no capabilities at all. Scaffolding the union of them and leaving you to delete half is how a starter ends up shipping a server nobody runs.
--mode=static (default) | --mode=fullstack | |
|---|---|---|
| Runs in production | Nothing | src/server.tsx, under esrun |
npm run build | The page prerendered into dist/index.html | Server and browser bundles into dist/ |
npm run build:spa | The page rendered in the browser | — |
| Grants | None — nothing runs | read, env, listen |
| Deploy | Any static host | dist/, whole |
Both modes render the same App through src/render.tsx, and both keep React Fast Refresh during npm run dev.
It asks, or it writes files and stops
On a terminal it asks which template, which mode if that template has more than one, the OTF axes where the template takes them, which package manager the project uses, and whether to install — as a menu you arrow through. The OTF starters choose behind one group entry, in a second menu; neither menu preselects — the template is the one answer worth choosing explicitly:
? Which Template? ❯ API A JSON API — a hello-world server under a narrow grant. No deps Library A publishable TypeScript package — .d.ts included, no runtime deps Micro-UI Micro apps with Micro-UI — framework-free UI with a tiny reactive core ReactJS React — a static site, or an app with a server of its own Vanilla TypeScript and the DOM — no framework, nothing it ships depends on OTF Web OTF Web starters — SPA, fullstack, docs or library ↑/↓ move · 1-9 jump · enter select · esc cancel
Menus show the proper name — SPA, not spa — while flags stay lowercase: what is chosen still resolves to the flag spelling, so --template=spa, SPA, and 1 all reach the same starter. Behind the group entry:
? Which OTF Web Starter? ❯ SPA An OTF Web single-page app — browser-only UI, static deploy, no server FullStack An OTF Web fullstack app — loaders, API routes, SSR on demand Docs An OTF Web documentation site — MDX docs, sidebar, search, optional blog Component Library An OTF Web component library — publishable components, esdev-tested ↑/↓ move · 1-9 jump · enter select · esc back
It is drawn where the cursor already is and is replaced in place by one line naming the answer, so the scrollback is left holding a transcript of what was asked and what was chosen rather than a cleared screen. Esc steps back to the previous question — the second OTF menu returns to the first — and Esc at the first question cancels: nothing is written, and the exit status is zero.
In a pipe or CI job, esdev uses the defaults without prompting — except the template, which has no default and must be named with --template. It still reports resolved package versions and whether scaffolding succeeded.
Every question has a flag, so nothing is only reachable by answering one:
esdev create my-app --template=react --mode=fullstack esdev create my-app --template=api --install=bun esdev create my-app --template=spa --package-manager=pnpm --no-install esdev create my-app --template=api --yes # defaults, no questions esdev create my-app --no-install
The manager question marks what this machine has — installed entries are choosable, the rest read as not installed and cannot be picked — and the scaffolded docs name the answer. The install question is then a yes/no for exactly that manager; where the manager came from a flag naming something not installed here, it lists what is instead. An explicit --install=<manager> answers both at once. Unattended it installs nothing: there is no lockfile yet to say which one this project uses, and guessing wrong leaves the wrong one behind — which is a reason not to guess, not a reason not to ask.
It still does no git init.
It never overwrites
A directory that already holds something is refused. --force writes among what is there and still leaves every existing file alone — it means "write here too", never "write over".
error: my-app is not empty.
Starting from existing code: esdev init
create starts from a template; init starts from a directory. An empty one gets the bare minimal setup — a greeting server in JS or TS, built and run by esdev:
esdev init my-app
It asks the name (defaulting to the directory), the language, the package manager, and whether to install — esdev init my-app --language=ts --package-manager=bun --install=bun answers all four. A directory with a project in it gets the one file it is missing instead: a working esdev.json with the detected entry, plus its types installed:
esdev init # adopt this directory esdev init --entry=src/server.ts
An esdev.json already there is refused outright, and each flow refuses the other's flags by name. Nothing is ever overwritten.
Options
--template=<name> | Which template, no default |
--mode=<name> | Which shape of it, where it has more than one: static or fullstack |
--language=<name> | OTF templates: js (default) or ts |
--styling=<name> | css or tailwind, for react, vanilla, micro-ui, preact, lit, spa and fullstack |
--blog, --no-blog | docs: keep the demo blog (default) or not |
--package-manager=<name> | The project's manager: npm, bun, pnpm, yarn — the docs name it |
--install[=<manager>] | Install after writing: npm, bun, pnpm, yarn |
--no-install | Write the files and stop |
-y, --yes | Take every default; never ask |
--list | List the templates and exit |
--force | Write into a directory that already holds something |
esdev create weather-app writes "name": "weather-app" into package.json and <title>weather-app</title> into the document. A directory name that npm would reject — My App — is lowercased and hyphenated on the way in.