Plugin context and ordering
The last argument of every handler, never this.
| Member | Description |
|---|---|
ctx.resolve(source, importer?, options?) | Asks the build's own resolver, mid-hook. null when nothing resolves. |
ctx.emit({ type, … }) | Adds a chunk or an asset to a build already running; returns a reference id. Refused in finish, where every bundle is already written. |
ctx.warn(msg) / info / debug | Diagnostics. Warnings come back from the build, prefixed with your plugin's name. |
ctx.error(msg) | Fails the build. It throws — it does not return, because you are saying the build cannot continue. |
ctx.isEntry | On resolve: whether this specifier is an entry. |
ctx.command | "build", "start", "test" or "run". Absent in a build a program starts with runtime:build. |
ctx.platform | "browser" or "server" ("node" for a runtime:build that asked for it): what the compiled code runs against. |
ctx.target | The project target being built, by name. Absent outside a project build. |
ctx.hot | true for a browser build in a hot dev loop, where modules are replaced in place (component refresh). |
ctx.bundle | On an html hook not ordered pre: what the build produced, fileName relative to the output directory (html). |
ctx.type | On transform: what the module is now — "css", "jsx", "js", … Not always what the extension says, since a pass ordered pre may already have changed it. |
It is an argument so that an arrow-function handler keeps it. Rollup's context-as-this is silently lost by an arrow, and a hook whose this.resolve is undefined fails a long way from the arrow that caused it.
It is also live only while its hook runs. Stash ctx and call it later and it throws — by then it may name a build that no longer exists.
Where a hook runs
One plugin serves every build and every command, so it is told where it is running rather than declared per target. These are facts, not modes: a framework's SPA, SSG or SSR is the plugin's own option, and the plugin combines it with ctx.platform.
export default (options) => ({ name: "framework", transform: { filter: { id: /\.jsx$/ }, handler(code, id, ctx) { const mode = ctx.platform === "server" ? options.mode : options.mode === "spa" ? "csr" : "hydrate"; return { code: compile(code, id, mode), type: "js" }; }, }, });
| Where | command | platform | target | hot |
|---|---|---|---|---|
esdev build, a server target | build | server | its name | false |
esdev start, a browser target | start | browser | its name | true unless --no-hot |
esdev build <entry> | build | the entry's | — | false |
esdev test | test | server | — | false |
esdev test --dom, --browser | test | browser | — | false |
esdev <file> | run | server | — | false |
A value that does not apply is absent, not null.
Pass { skipSelf: true }, or your hook resolves through itself and the build never finishes:
const found = await ctx.resolve(source, importer, { skipSelf: true });
emit puts a file beside the output, or adds an entry to a build in flight:
ctx.emit({ type: "asset", fileName: "meta.json", source: '{"built":true}' }); ctx.emit({ type: "chunk", id: "/app/admin.js" });
order
transform: { order: "pre", filter: { id: /\.mdx$/ }, handler }
"pre" runs before the unordered plugins and "post" after, for when one pass has to see a module before another does. Within a group, plugins run in the order you listed them.
order: pre-one -> normal -> post-one
It orders you against esdev's own passes too, not only against the other plugins. esdev:css-modules runs post: it finishes whatever the plugins made CSS, and steps aside for a module whose ctx.type is no longer "css". A plugin that turns a stylesheet into JavaScript says pre, so a plugin that expects CSS still sees the stylesheet before it does:
transform: { order: "pre", filter: { id: /\.css$/ }, handler: (code, id) => ({ code: compileTailwind(code, id), type: "js" }), }
What the build hands back
const { output, watchFiles, warnings } = await bundle.generate({ format: "esm" });
watchFiles is every file the build read plus every file a plugin declared, which is what makes an incremental dev server possible: pair it with runtime:watch and drop the three cached chunks whose dependencies changed rather than all forty. warnings carries the bundler's and yours.
When you get it wrong
The declaration is checked when build() is called, not at the generate() three lines later, so the rejection lands on the line that wrote it.
| Written | What it says |
|---|---|
transform(code, id) {} | p.transform: a hook is an object, not a function — write { handler(...) {} }, optionally with filter and order |
transfrom: { … } | p: unknown hook "transfrom". Did you mean "transform"? |
start: { filter: … } | p.start: this hook runs once, for the whole build, so it cannot be filtered |
load: { filter: { code: … } } | p.load: only transform can filter on code — it is the only hook given any |
order: "first" | p.transform: order must be "pre" or "post", got "first" |
handler: () => "export {}" | transform must return an object or null — return { code } instead |
A handler that throws fails the build with what it threw, stack and all — a plugin error arriving as a bare "build failed" would be the worst outcome of running hooks on a different thread from the bundler.