Writing a plugin

A plugin puts your own code inside an esdev build: it can invent a module, rewrite one that exists, or tell the watcher about a file the graph cannot discover. Project plugins are declared in esdev.json and apply to builds, tests and runs alike (where plugins run); programmatic builds pass them to build().

Read only the part you need

Plugins run in the build process and are esdev-only. The sidebar lists the guide pages by the decisions you make while writing one.

How this compares

Eight hooks against rollup's twenty-odd — each one is a promise whatever bundler sits behind the contract has to keep, so the list is short on purpose. The table below is the delta against the systems plugin authors arrive from; the linked pages are the full description.

Vite 8 and esdev share the same foundation — Rolldown for bundling, Oxc for transforms — so none of the difference below is the engine. It is the contract on top: Rolldown kept Rollup's plugin API as its own, while esdev defines a smaller one against it.

esdevRollupVite 8
Hooksstart, resolve, load, transform, end, bundle, html, finish (lifecycle)20+: options, buildStart, resolveId, load, transform, moduleParsed, buildEnd, generateBundle, writeBundle, closeBundle, watchChange…Rolldown's hooks plus Vite's own: config, configResolved, configureServer, transformIndexHtml, handleHotUpdate…, per environment
html hookBuilt in, with pre/post orderingNonetransformIndexHtml — built in and powerful, Vite-only
finish hookRuns after every target is staged, before anything moves into place — for sitemaps, manifests, prerendered pages; an error fails the build with nothing deployedNo equivalent (writeBundle runs per bundle, after files are written)Same as Rollup
bundle hookRead-only: the chunk graph without the bytes, so one plugin cannot invalidate another's mapsgenerateBundle hands over mutable chunksSame as Rollup
Virtual modules{ id, virtual: true } — explicit, no conventionA NUL byte glued to the idSame NUL convention, encoded as /@id/__x00__… in dev URLs
Filter{ id, code }, matched host-side before the hook crosses into the isolate (ordering){ filter: { id, code }, handler } since 4.38Same object-hook filters since 6.3
Filter semanticsA string is an exact match; an empty or uncompilable filter is refused at declarationA string is a picomatch glob; nothing is refusedSame as Rollup, plus moduleType and composable and/or/not via pluginutils
ContextA ctx argument — arrow-safethis (lost by an arrow function)Same this
DependenciesReturned as dependsOn — it cannot be forgotten (dependencies)this.addWatchFile(), a call you can forgetSame as Rollup
SandboxGuest code under the capability model: a plugin that reads a file needs FileRead (runtime)Full Node accessFull Node access
ConfigData: esdev.json names the module and its JSON optionsExecutable config, arbitrary code pre-buildExecutable config
MistakesRefused at declaration with a fix: unknown hooks get a did-you-mean, bare-function hooks and string returns are named, uncompilable filters say how to rewrite (troubleshooting)Unknown hooks and bad returns are silently ignoredSame as Rollup

Two honest gaps. The filter is compiled by Rust's regex, so lookbehind and backreferences are refused rather than run — and unlike Rolldown there are no include/exclude or composable filters, only flat id/code lists. Porting a rollup plugin still means rewriting its filters alongside its NUL-prefixed ids. And there is no plugin registry yet; esdev.json can name a bare package, but discovery is word of mouth.

Last updated on
Edit this page