Should I upgrade to Prettier 3.0+ ESM-Only Plugin API — CommonJS Plugins No Longer Load now?
Prettier 3.0 switched to async-only, ESM-only plugin loading. Plugins written as CJS modules fail silently or throw. Teams using custom Prettier plugins or older community plugins must port them to ESM or pin Prettier <3.
Blockers
- breaking_change_in: capability/cjs-plugin-loading → package/prettier
- breaking_change_in: capability/async-public-api → package/prettier
- requires_version: format/esm → package/prettier
- Lock-in via package/prettier
Who this is for
- monorepo
- small-team
- enterprise
Candidates
Port Plugin to Pure ESM
Convert the plugin to ESM by replacing module.exports with export default, adding type: module to its package.json, removing the parsers second argument from parse(), and updating the embed() printer method to Prettier 3's incompatible new signature. All calls to Prettier's own API inside the plugin and its tests must be awaited because prettier.format() and every other public method now returns a Promise. This is the approach recommended by the official Prettier v3 plugin migration wiki and is fully compatible with Prettier 3.0 through the current 3.8.1 release.
When to choose
Best for small-team or monorepo situations where you own the plugin source and all downstream consumers have already upgraded to Prettier 3+. The single most decisive factor is whether you need to keep supporting Prettier 2.x consumers from the same package — if not, pure ESM eliminates ongoing dual-format maintenance.
Tradeoffs
Pros: cleanest long-term solution, fully supported by Prettier 3.x, enables async parsers and the new embed() API. Cons: Prettier 2.x cannot load a pure-ESM plugin via its synchronous require()-based loader, so consumers still on Prettier 2 will break; TypeScript configs targeting CommonJS output will silently emit CJS files that still fail under Prettier 3's import() loader.
Cautions
TypeScript projects that emit CommonJS by default will continue to produce .js files that Node treats as CJS even after code is refactored to use export default — verify the build output format explicitly before publishing. Test harnesses that call prettier.format() synchronously will throw after migration because the entire public API is now async-only with no synchronous fallback.
Dual-Mode Package via Conditional Exports
Publish the plugin with both an ESM entry (import condition) and a CJS entry (require condition) in the exports field of package.json. When Prettier 3 loads the plugin via import(), Node resolves the import condition and loads the ESM build; Prettier 2 or any CJS caller using require() loads the CJS build. The ESM entry must still implement all other Prettier 3 API changes: the new embed() signature, the removed parse() second argument, and async-capable parsers. As of 2026-03-18, this is the primary approach for plugin authors who cannot break Prettier 2.x consumers.
When to choose
Best for enterprise or open-source teams that publish a shared plugin consumed by projects at different Prettier versions simultaneously. Use when forcing a synchronised Prettier upgrade across all consumers is not feasible and breaking Prettier 2.x callers is unacceptable.
Tradeoffs
Pros: one package satisfies both Prettier 2 and Prettier 3 consumers with no forced lockstep migration. Cons: doubles build complexity — requires tooling that emits both .mjs and .cjs artifacts plus conditional exports wiring; the ESM entry must still fully implement the new Prettier 3 plugin API, so the migration work is not avoided, only made backward-compatible.
Cautions
Omitting the exports field entirely and relying solely on main causes Prettier 3's import() to resolve a CJS file, which may fail on Node versions that do not support require(ESM) without a flag. Validate both code paths with explicit smoke tests before publishing — build systems frequently produce only one output format even when configured for dual-mode.
Pin Prettier to 2.x
Lock the prettier dependency to an exact 2.x release (e.g. 2.8.8) in package.json to retain the synchronous CJS-based plugin loader. Prettier 2.x loads plugins via require(), so unmodified CJS plugins continue to work without any code changes. As of 2026-03-18, Prettier 2.x receives no maintenance releases and all active development targets the 3.x line (currently 3.8.1), meaning new language support, parser fixes, and API improvements are not available.
When to choose
Best for cost-sensitive or small-team situations where the blocking plugin is a third-party dependency whose maintainer has not yet published a Prettier 3-compatible release and forking is not practical. Treat as a temporary hold and plan to lift the pin as soon as upstream ships ESM support.
Tradeoffs
Pros: zero code changes required, unblocks the team immediately, all existing CJS plugins work without modification. Cons: frozen on an unmaintained version with no access to Prettier 3.x language features or parser improvements; creates version skew risk in monorepos where workspaces may implicitly require 3.x-only plugin behaviour.
Cautions
In monorepos with a package manager that hoists dependencies, a stray caret range on prettier elsewhere in the workspace (e.g. ^3.0.0) can cause Prettier 3 to be hoisted and shadow the pinned 2.x version for packages that depend on it — always use an exact pin and audit hoisting behaviour. Prettier 2.x's synchronous API means any plugin code written to the Prettier 3 async interface will be incompatible until the pin is lifted.
Try with your AI agent
$ npm install -g pocketlantern $ pocketlantern init # Restart Claude Code, Cursor, or your MCP client, then ask: # "Should I upgrade to Prettier 3.0+ ESM-Only Plugin API — CommonJS Plugins No Longer Load now?"