Prettier 28 ๐จ Prettier 3.x Changes โ ESM migration & custom parser removal
Prettier 3.0 was released on July 5, 2023, and it was the first major version to make the entire codebase ECMAScript Modules. The migration was not a cosmetic change. It affected how the library is loaded, how plugins are written, and how the API is called. The three breaking changes that matter most for a migration are the ESM-first source, the removal of the synchronous API, and the removal of the custom parser API. A project that upgrades without addressing all three will see errors that point to the wrong cause: a require failure that looks like a missing package, a Promise object where a string was expected, and a parser option that is silently ignored.
The custom parser API was the older mechanism for adding a language to Prettier. It was a function that received the text and the parsers and returned an AST. Prettier 3.0 removed it entirely, and the replacement is the plugin API, which is a module with languages, parsers, and printers exports . The removal is not a deprecation with a warning. A configuration that passes a custom parser function to the parser option in Prettier 3.0 does not work, and the error is a type mismatch rather than a migration hint.
Key point: Prettier 3.0 migrated to ESM, removed the synchronous API, and removed the custom parser API. The plugin API is the replacement for the custom parser. A CommonJS project can still use Prettier as a library, but the API is asynchronous and the plugins must follow the plugin contract.
Why the migration is more than a version bump
The ESM source problem. Prettier 3.0’s source code is ESM, and the package is published with the exports map that points to the ESM build. A CommonJS project that does require('prettier') still works because the package provides a CommonJS compatibility entry, but the internal loading path changed . The failure mode is not a require error; it is a plugin that was written as a CommonJS module and expects the Prettier 2.x plugin interface. The plugin interface was overhauled, and the new interface expects an ESM module or a module that can be loaded asynchronously.
The synchronous API removal. Prettier 2.x had a format function that returned a string. Prettier 3.0’s format returns a Promise<string>. The change is a consequence of the async parser support, which is a consequence of the ESM migration. A script that calls prettier.format(source, options) and passes the result to a function that expects a string receives a Promise object, and the error is a type error in the downstream function, not in Prettier . The fix is to await the result, and the migration touches every place that calls the API.
The custom parser removal. The custom parser API was the older mechanism for adding a language. It was a function that received the text and returned an AST. Prettier 3.0 removed it, and the replacement is the plugin API with languages, parsers, and printers . A project that used a custom parser must rewrite it as a plugin, and the rewrite is not a rename; it is a different contract. The parsers object in the plugin API contains a parse function and the locStart/locEnd functions, and the printers object contains the print function that returns a Doc rather than a string.
The trade-off. The ESM migration and the async API are the price of the plugin architecture. The plugin architecture is what allows Prettier to support multiple languages without bundling every parser, and the async parser support is what allows a plugin to spawn a process or load a parser lazily. The custom parser removal is the price of the plugin architecture: the custom parser was a shortcut that bypassed the plugin contract, and the shortcut is gone.
a. The ESM migration and the CommonJS compatibility
The migration is not a forced ESM conversion for the consumer. The Prettier team’s announcement states that “when using Prettier as a library, you can still use it as CommonJS as well” . The compatibility entry is the mechanism that allows a CommonJS project to keep using require('prettier'), but the internal module graph is ESM. The compatibility is a bridge, and the bridge is the reason a CommonJS project can upgrade without converting its entire codebase.
// A CommonJS project can still require Prettier.
const prettier = require('prettier');
// The format function is asynchronous in 3.0.
async function formatCode(source) {
return await prettier.format(source, { parser: 'babel' });
}
The configuration file loading is the other ESM surface. Prettier 3.0 supports prettier.config.mjs and .prettierrc.mjs in addition to the .js forms . A project that uses a .js configuration with module.exports still works, but a project that wants to use the ESM syntax in the configuration file must use the .mjs extension or set "type": "module" in the nearest package.json. The extension is the signal, and the signal is the migration decision.
b. The async API and the custom parser removal
The async API is the change that touches the most code. Every call to prettier.format must be awaited, and every function that wraps the call must be async. The prettier.check function is also async, and the resolveConfig function is async. The migration is a mechanical change, but it is a change in every file that calls the API.
// Prettier 2.x: synchronous
const formatted = prettier.format(source, { parser: 'babel' });
// Prettier 3.0: asynchronous
const formatted = await prettier.format(source, { parser: 'babel' });
The custom parser removal is the change that affects the plugins. A custom parser was a function that received the text and the parsers and returned an AST. The plugin API is a module with three exports, and the parse function is inside the parsers object.
// A Prettier 3.0 plugin
export const languages = [
{
name: 'MyLanguage',
parsers: ['my-parser'],
extensions: ['.mylang'],
},
];
export const parsers = {
'my-parser': {
parse: (text) => parseMyLanguage(text),
locStart: (node) => node.start,
locEnd: (node) => node.end,
},
};
export const printers = {
'my-ast': {
print: (path, options, print) => printMyNode(path.getValue(), path, options, print),
},
};
The printers object is the second half of the plugin. The print function returns a Doc, not a string. The Doc is built with the prettier.doc.builders helpers, and the renderer decides the line breaks. The custom parser API did not have a printer; it returned an AST that Prettier’s built-in printers consumed. The plugin API requires the plugin to provide the printer, which is the reason the migration is a rewrite rather than a rename.
Complete Example Session
// ============================================
// PART 1: THE ESM MIGRATION
// ============================================
// The package.json of Prettier 3.0 is ESM.
// A CommonJS project can still require it.
const prettier = require('prettier');
// The configuration file can be .mjs.
// prettier.config.mjs
export default { semi: true, singleQuote: true };
// ============================================
// PART 2: THE ASYNC API
// ============================================
// Prettier 2.x: synchronous
// const formatted = prettier.format(source, { parser: 'babel' });
// Prettier 3.0: asynchronous
async function formatCode(source) {
return await prettier.format(source, { parser: 'babel' });
}
// ============================================
// PART 3: THE CUSTOM PARSER REMOVAL
// ============================================
// Prettier 2.x: a custom parser function
// const parser = (text, parsers, options) => parseMyLanguage(text);
// prettier.format(source, { parser });
// Prettier 3.0: the plugin API
export const languages = [{ name: 'MyLanguage', parsers: ['my-parser'], extensions: ['.mylang'] }];
export const parsers = {
'my-parser': {
parse: (text) => parseMyLanguage(text),
locStart: (node) => node.start,
locEnd: (node) => node.end,
},
};
export const printers = {
'my-ast': {
print: (path, options, print) => printMyNode(path.getValue(), path, options, print),
},
};
// ============================================
// PART 4: THE PLUGIN REGISTRATION
// ============================================
// .prettierrc
{
"plugins": ["./my-plugin.js"]
}
// The plugin is registered in the plugins array.
// The custom parser API is gone; the plugin is the replacement.
// ============================================
// PART 5: THE COMMONJS BRIDGE
// ============================================
// A CommonJS project can use Prettier as a library.
// The require() call works.
// The format() call is still asynchronous.
// ============================================
// PART 6: THE CONFIGURATION FILE
// ============================================
// Prettier 3.0 supports .mjs config files.
// prettier.config.mjs
export default { semi: true };
// The .js config with module.exports still works.
// module.exports = { semi: true };
// ============================================
// PART 7: THE PLUGIN INTERFACE OVERHAUL
// ============================================
// The plugin interface was overhauled.
// The async parsers are supported.
// The ESM plugins are supported.
// A plugin written for Prettier 2.x must be updated.
// ============================================
// PART 8: THE PRINTER CONTRACT
// ============================================
// The printer returns a Doc, not a string.
// The Doc is built with the doc builders.
import { doc } from 'prettier';
const { group, concat, line } = doc.builders;
// ============================================
// PART 9: THE TRAILING COMMA DEFAULT
// ============================================
// The default trailingComma changed to "all".
// A project that wants the old behavior sets it explicitly.
{ "trailingComma": "es5" }
// ============================================
// PART 10: THE CHECKLIST
// ============================================
// [ ] The format calls are awaited.
// [ ] The configuration file uses the .mjs extension if it uses ESM.
// [ ] The custom parser is rewritten as a plugin.
// [ ] The plugin has languages, parsers, and printers.
// [ ] The printer returns a Doc.
// [ ] The trailingComma default is accepted or overridden.
The ten parts covered the ESM migration, the async API, the custom parser removal, the plugin registration, the CommonJS bridge, the configuration file, the plugin interface, the printer contract, the trailing comma default, and the checklist. The through-line is that the three changes are connected, and the migration is a rewrite of the plugin and the API calls.
Quick Reference
The three breaking changes
| Change | Prettier 2.x | Prettier 3.0 |
|---|---|---|
| Source | CommonJS | ESM |
format() | Synchronous | Asynchronous |
| Custom parser | Function | Plugin API |
The ESM surface
| Surface | Prettier 2.x | Prettier 3.0 |
|---|---|---|
| Source | CJS | ESM |
| Library use | CJS | CJS bridge |
| Config file | .js | .js, .mjs |
The plugin API
| Export | Purpose |
|---|---|
languages | The file extensions and parser names |
parsers | The parse, locStart, and locEnd functions |
printers | The print function that returns a Doc |
The other changes
| Change | Detail |
|---|---|
trailingComma default | Changed to "all" |
Flow syntax in babel | Removed |
| Markdown whitespace | No space between CJK and Latin |
Best Practices
โ Do This:
// Await the format call
const formatted = await prettier.format(source, { parser: 'babel' }); // โ
// Use the plugin API for a custom language
export const parsers = { 'my-parser': { parse, locStart, locEnd } }; // โ
// Set the trailingComma explicitly if the old default is wanted
{ "trailingComma": "es5" } // โ
โ Don’t Do This:
// Use the synchronous API
const formatted = prettier.format(source, { parser: 'babel' }); // โ
// Pass a custom parser function
prettier.format(source, { parser: myCustomParser }); // โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
The result is a Promise | format() is async in 3.0 | Await the call |
| The custom parser is ignored | The API was removed | Rewrite as a plugin |
| The plugin fails to load | The plugin is CJS-only | Rewrite as ESM or use the async loading |
| The trailing commas appear | The default changed to "all" | Set trailingComma: "es5" |
The Flow syntax in .js fails | The babel parser no longer detects Flow | Use the flow parser or the @flow pragma |
Real-World Examples
1. The async format
const formatted = await prettier.format(source, { parser: 'babel' });
2. The plugin languages
export const languages = [{ name: 'MyLanguage', parsers: ['my-parser'], extensions: ['.mylang'] }];
3. The plugin parsers
export const parsers = { 'my-parser': { parse, locStart, locEnd } };
4. The plugin printers
export const printers = { 'my-ast': { print } };
5. The config file
// prettier.config.mjs
export default { semi: true };
6. The CommonJS bridge
const prettier = require('prettier');
7. The trailing comma override
{ "trailingComma": "es5" }
8. The plugin registration
{ "plugins": ["./my-plugin.js"] }
9. The Doc builder
import { doc } from 'prettier'; const { group, concat } = doc.builders;
10. The async check
const isFormatted = await prettier.check(source, { parser: 'babel' });
Visual
The Three Changes
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ESM, ASYNC, AND THE CUSTOM PARSER REMOVAL โ
โ โ
โ Prettier 2.x Prettier 3.0 โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ CJS source โ โ ESM source โ โ
โ โ Sync format() โ โ Async format() โ โ
โ โ Custom parser โ โ Plugin API โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ The three changes are connected. โ
โ The ESM migration enabled the async API. โ
โ The async API enabled the plugin architecture. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Custom Parser vs the Plugin
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE CONTRACT CHANGED โ
โ โ
โ Custom parser (removed): โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ (text, parsers, โ Returns an AST โ
โ โ options) => AST โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Plugin API (replacement): โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ languages โ The extensions and parsers โ
โ โ parsers โ The parse and loc functions โ
โ โ printers โ The print function (returns Doc) โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ The plugin is a module, not a function. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Prettier 3.0 release | July 5, 2023 |
| Source migration | ESM |
| Library compatibility | CommonJS bridge |
format() | Asynchronous |
| Custom parser API | Removed |
| Plugin API | languages, parsers, printers |
trailingComma default | "all" |
Key takeaways:
- Prettier 3.0 migrated its source to ESM. A CommonJS project can still use Prettier as a library, but the internal module graph is ESM and the plugin interface was overhauled . The configuration file can be
.mjsif the project uses the ESM syntax . - The API is asynchronous.
format()returns aPromise<string>, and every call must be awaited. The synchronous API was removed, and a script that uses the return value directly receives aPromiseobject . - The custom parser API was removed. The replacement is the plugin API, which is a module with
languages,parsers, andprintersexports. Theprintersobject contains theprintfunction that returns aDoc, not a string . - The
trailingCommadefault changed to"all". A project that wants the Prettier 2.x behavior must settrailingComma: "es5"explicitly . - The plugin interface supports async parsers and ESM plugins. A plugin written for Prettier 2.x must be updated, and the migration guide is the reference. The async parser support is the reason the API became asynchronous.
Remember: Prettier 3.0 is a major version with three connected breaking changes. The ESM migration changed the module format, the async API changed the call pattern, and the custom parser removal changed the plugin contract. A CommonJS project can still use the library, but the format call is awaited, and the custom parser is rewritten as a plugin. The three changes are not independent; they are the layers of the same architectural shift.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!