| | |

Prettier 28 ๐ŸŽจ Prettier 3.x Changes โ€” ESM migration & custom parser removal

โฑ๏ธ Estimated read: 11 min | |

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

ChangePrettier 2.xPrettier 3.0
SourceCommonJSESM
format()SynchronousAsynchronous
Custom parserFunctionPlugin API

The ESM surface

SurfacePrettier 2.xPrettier 3.0
SourceCJSESM
Library useCJSCJS bridge
Config file.js.js, .mjs

The plugin API

ExportPurpose
languagesThe file extensions and parser names
parsersThe parse, locStart, and locEnd functions
printersThe print function that returns a Doc

The other changes

ChangeDetail
trailingComma defaultChanged to "all"
Flow syntax in babelRemoved
Markdown whitespaceNo 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

PitfallWhy It HappensFix
The result is a Promiseformat() is async in 3.0Await the call
The custom parser is ignoredThe API was removedRewrite as a plugin
The plugin fails to loadThe plugin is CJS-onlyRewrite as ESM or use the async loading
The trailing commas appearThe default changed to "all"Set trailingComma: "es5"
The Flow syntax in .js failsThe babel parser no longer detects FlowUse 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

ItemValue
Prettier 3.0 releaseJuly 5, 2023
Source migrationESM
Library compatibilityCommonJS bridge
format()Asynchronous
Custom parser APIRemoved
Plugin APIlanguages, 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 .mjs if the project uses the ESM syntax .
  • The API is asynchronous. format() returns a Promise<string>, and every call must be awaited. The synchronous API was removed, and a script that uses the return value directly receives a Promise object .
  • The custom parser API was removed. The replacement is the plugin API, which is a module with languages, parsers, and printers exports. The printers object contains the print function that returns a Doc, not a string .
  • The trailingComma default changed to "all". A project that wants the Prettier 2.x behavior must set trailingComma: "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.

Was this helpful?


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!