esdev create

Shell
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:

Shell
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

Shell
esdev create --list
esdev create my-app --template=react --mode=fullstack
reactReact — a static site, or an app with a server of its own
apiA JSON API under a narrow grant. Nothing it ships depends on
honoAn HTTP app on Hono — routes, validation and pages behind one server
elysiaAn HTTP app on Elysia — typed routes and validation behind one server
vanillaTypeScript and the DOM — no framework
micro-uiMicro apps with Micro-UI — framework-free UI with a tiny reactive core
preactPreact on the DOM — React's API in three kilobytes
litLit web components — shadow DOM, client-rendered
libA publishable package — .d.ts and exports wired
spaAn OTF Web single-page app — browser-only UI, static deploy, no server
fullstackAn OTF Web fullstack app — loaders, API routes, SSR on demand
docsAn OTF Web documentation site — MDX docs, sidebar, search, optional blog
libraryAn 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).

Shell
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:

Shell
esdev create my-app --template=spa --language=ts --styling=tailwind
esdev create my-app --template=docs --no-blog
--language=js|tsspa, fullstack, docs, library — JavaScript (default) or TypeScript
--styling=css|tailwindspa, fullstack — plain CSS or Tailwind v4
--blog, --no-blogdocs — 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!"}.

TEXT
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 productionNothingsrc/server.tsx, under esrun
npm run buildThe page prerendered into dist/index.htmlServer and browser bundles into dist/
npm run build:spaThe page rendered in the browser—
GrantsNone — nothing runsread, env, listen
DeployAny static hostdist/, 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:

TEXT
? 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:

TEXT
? 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:

Shell
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".

TEXT
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:

Shell
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:

Shell
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-blogdocs: 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-installWrite the files and stop
-y, --yesTake every default; never ask
--listList the templates and exit
--forceWrite into a directory that already holds something
The name comes from the directory

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.

Last updated on
Edit this page