esdev start
esdev start
It is esdev build on a loop. A dev build differs from a release build in four ways and no more: process.env.NODE_ENV is "development", nothing is content-hashed, the browser bundle is built so a module can be replaced in place — and it is written under the dev directory (.dev by default) rather than where esdev build deploys.
On a change: rebuild, restart the server if its bundle moved, and patch the page — or reload it if nothing accepted the change. Read the details in Hot module replacement.
What it prints
A banner once it is listening and built, then one line per save — nothing else. Build reports and errors go to stderr, so stdout stays the program's:
esdev <version> — ready in <duration> → Local: http://localhost:5173/ ✓ hot-swapped <count> modules in <duration> ✓ reloaded in <duration> ✓ restarted server in <duration>
A project with a server of its own names the app's URL instead of the endpoint's. A failed build prints its error; the loop keeps running, and the next save gets its own line. See Build failures for browser errors and output behavior.
The server is yours
"dev": { "run": "server" }
That names the target whose output is your server. esdev runs it as a child process with esdev's normal development permissions and restarts it with a SIGTERM — the graceful stop production gets, so a request in flight when you save is answered rather than dropped. Put production grants on the esrun command that deploys the built program.
Declare the app's default port under dev.app.port:
"dev": { "run": "server", "app": { "port": 8080 } }
The server should read PORT and use this value as its fallback. If that port is already in use, esdev chooses a free one and sets PORT in the child. Use --port=<n> to pin the app to a particular port; that port must be free.
It is the same build production runs, in development profile. No dev server stands in for it, nothing wraps it, and there is no second code path that exists only on your machine. What differs is only where its bytes land: every target output is mirrored under .dev/ (dist/server.js is rebuilt as .dev/dist/server.js), so a save never overwrites the deployment. Run esdev build again before deploying — dist/ is only ever a release build now.
"dev": { "outDir": "tmp-dev" }
names another directory for the loop's builds. It must stay inside the project and clear of every target's out and outdir; anything overlapping is refused when the file is read.
A project with no server of its own — a static site or single-page app — has nothing to run, so esdev serves the output directory itself: files, an index.html fallback for client-side routes, and nothing else.
Build failures
esdev: src/App.tsx: Unexpected token
The running server keeps serving when a rebuild fails. The rebuild happens before the server is stopped, so it also answers while the build runs.
The error is sent to the browser and shown in an overlay until a successful update clears it. A page that opens or reconnects during the failure receives the current error too.
Development outputs are written in place: a failed build may already have updated some files. The loop's output directory is separate from the release output. esdev build stages release outputs before replacing them.
The update channel
Every built document gets a WebSocket to ws://127.0.0.1:<port>/@esdev/hmr. It acts on a module patch, stylesheet swap, or reload. It is esdev's endpoint rather than your application's, so nothing dev-only reaches your source and the file you edit is never written to.
An update is sent after a restart finishes. The client reconnects on its own, backing off to a 5 s ceiling, because a restarted dev server is ordinary. The port is written into the document when it is built, so this works while the endpoint keeps the same port. If you stop and start esdev again and its unpinned endpoint chooses a different port, reload the page; the old document cannot discover that new endpoint. For a frontend project, use --port=<n> to pin the endpoint.
Options
--port=<n> | The port you open |
--no-hot | Reload the page instead of patching changed modules |
--config=<path> | Read this instead of ./esdev.json |
--allow-read=<paths> | Also watch explicitly granted read paths |
--shutdown-grace=<ms> | How long the server may drain on a restart |
"dev": { "run": "server", "watch": { "targets": ["server", "web"], "paths": ["./config"] }, "app": { "port": 8080 }, "outDir": ".dev" }
These dev settings control what the loop rebuilds and serves:
watch.targetsselects targets to rebuild. If omitted, esdev rebuilds every target except"then": "run"steps, which run only inesdev build. Theruntarget is always built. List a step here to run it on every save.watch.pathsadds watched paths, resolved relative toesdev.json.serveselects the directory to serve when noruntarget is configured.app.portsets the app server's fallback port.server.portpins esdev's listener for a frontend-only project.
In full-stack mode, the app owns the browser endpoint; esdev's listener is an internal HMR channel.
Forward API requests
A frontend that calls a separate API server with relative URLs (fetch("/api/users")) names it in dev.server.proxy:
"dev": { "server": { "proxy": { "/api": "http://localhost:8080" } } }
A request whose path starts with a prefix goes to its target, the longest prefix first; everything else is served from the build. The target's path is prepended, so "http://localhost:8080/v1" receives /v1/api/users. Bodies, streamed responses, server-sent events and WebSocket upgrades pass through as they are. The target sees its own Host, with the page's in X-Forwarded-Host. An https target is verified like fetch verifies one, and a target that is not running answers 502 with its address.
It applies where esdev serves the page: esdev start without dev.run, and esdev preview. With dev.run, the application owns the endpoint and routes its own requests, so the key is refused there.
What is watched
esdev start watches the project root, every path under dev.watch.paths or passed with --allow-read, and the build inputs those paths may contain. The dev directory is always excluded, which covers every target output at once since the loop mirrors them all underneath it — so a build cannot trigger itself. Rules in the project's root .gitignore also exclude matching paths; other directory names are not hardcoded exclusions.
That includes modules and the things a build consumes that a run does not: index.html, stylesheets, images and fonts. An explicit allowed path is both part of the watch list. An unscoped --allow-read grants access but does not identify a finite directory to watch.
For the standalone command's narrower rules, see Watch mode.
Preview the release
Use Previewing a release after esdev build to check production-mode output, hashed URLs and the static server behavior.