Prettier 25 ๐จ Plugin Architecture โ Extending Prettier with custom parsers
Prettier’s plugin architecture is the mechanism that turns a JavaScript and CSS formatter into a multi-language tool. A plugin is a module that exports a languages array, a parsers object, and a printers object. The languages array declares which file extensions and filenames the plugin handles. The parsers object contains the parse function that turns source text into an abstract syntax tree (AST), along with the locStart and locEnd functions that tell Prettier where each node begins and ends. The printers object contains the print function that turns the AST back into a document, and the document is what Prettier renders as formatted text . The plugin is the entire extension surface; there is no separate registration step beyond adding the plugin to the Prettier configuration.
The custom parser is the first half of a plugin, and it is the half that most plugin authors underestimate. The parser must produce an AST that the printer can consume, and the AST must include location information for every node. Prettier uses the locations to attach comments to the correct node, to implement --range-start and --range-end, and to support the --cursor-offset option. A parser that omits the locations produces a plugin that formats the code but fails on every feature that depends on position.
Key point: A Prettier plugin exports languages, parsers, and printers. The parsers object contains the parse function and the locStart/locEnd functions. The printers object contains the print function that returns a Prettier Doc. The plugin is registered by adding it to the plugins array in the Prettier configuration.
Why the plugin architecture separates parsers and printers
The AST portability problem. A parser produces an AST, and the AST is specific to the language and the parser library. The printer consumes the AST and produces a document. The two are separated because the same printer can sometimes work with multiple parsers, and the same parser can sometimes work with multiple printers. The separation is the mechanism that allows a plugin to use an existing parser libraryโphp-parser for PHP, Ripper for Rubyโand write a printer that understands the resulting AST . The plugin author does not have to write a parser from scratch; the author writes the printer that turns the existing AST into Prettier’s document format.
The document format problem. Prettier’s print function does not return a string. It returns a Doc, which is a tree of document nodes that describe the layout. The Doc can contain group, indent, line, softline, hardline, and concat nodes, and Prettier’s renderer decides where to break the lines based on the printWidth option . The document format is what makes Prettier’s formatting consistent across languages: the plugin describes the structure, and Prettier decides the line breaks. A plugin that returns a string bypasses the renderer and produces output that does not respect the line width or the indentation rules.
The comment attachment problem. Comments are often not part of a language’s AST. Prettier has a comment attachment algorithm that uses the locStart and locEnd functions to find the node that each comment belongs to, attaches the comment to that node, and deletes the top-level comments property . The plugin can override the algorithm with canAttachComment, handleComments, and printComment, but the default algorithm works for most languages. The comment attachment is the reason the parser must produce accurate locations; a comment that is attached to the wrong node is printed in the wrong place, and the formatted output is wrong.
The trade-off. The plugin architecture is more complex than a simple text transformation. The parser must produce an AST, the printer must produce a Doc, and the locations must be accurate. The complexity is the cost of the features: the line-width rendering, the comment attachment, the range formatting, and the cursor offset. A plugin that does not need those features can return a simple Doc, but a plugin that wants the full Prettier experience must implement the full contract.
a. The plugin structure and the custom parser
A plugin is a JavaScript module with three exports. The languages array declares the file types, the parsers object contains the parser, and the printers object contains the printer.
// my-plugin.js โ the plugin structure
export const languages = [
{
name: 'MyLanguage',
parsers: ['my-parser'],
extensions: ['.mylang'],
},
];
export const parsers = {
'my-parser': {
// The parse function receives the source text and the options.
// It returns an AST.
parse: (text, options) => {
return parseMyLanguage(text);
},
// The locStart function returns the start offset of the node.
locStart: (node) => node.start,
// The locEnd function returns the end offset of the node.
locEnd: (node) => node.end,
},
};
export const printers = {
'my-ast': {
// The print function receives the path and the options.
// It returns a Doc.
print: (path, options, print) => {
const node = path.getValue();
// The print function returns a Doc, not a string.
return printMyNode(node, path, options, print);
},
},
};
The parse function must return an AST with a type property on every node. The locStart and locEnd functions must return the character offsets of the node’s start and end. The print function must return a Doc, which is built with the prettier.doc.builders helpers.
// my-plugin.js โ the parse function with a simple parser
import { parse } from 'some-parser-library';
function parseMyLanguage(text) {
const ast = parse(text);
// The AST must have a type on every node.
// The parser library must provide the locations.
return ast;
}
The parser can be asynchronous in Prettier 3.0. The parse function can return a Promise that resolves to the AST. The async parser is the mechanism for a plugin that spawns a process or makes a network request .
b. The printer and the document format
The printer receives the AST and returns a Doc. The print function is called recursively for each node, and the print argument is the function that prints the child nodes.
// my-plugin.js โ the printer
import { doc } from 'prettier';
const { group, indent, line, softline, hardline, concat } = doc.builders;
function printMyNode(node, path, options, print) {
switch (node.type) {
case 'Program':
// The Program node contains the statements.
// The print function is called for each child.
return concat([
// The path.map function iterates over the children.
path.map(print, 'body'),
hardline,
]);
case 'VariableDeclaration':
// The VariableDeclaration node contains the identifier and the value.
return group(concat([
node.kind,
' ',
path.call(print, 'id'),
' = ',
path.call(print, 'init'),
';',
]));
default:
return '';
}
}
The group builder tells Prettier that the content should be printed on one line if it fits, and broken across lines if it does not. The line builder is a space that becomes a newline when the group breaks. The softline builder is nothing when the group fits and a newline when it breaks. The hardline builder is always a newline. The indent builder increases the indentation for the content inside it.
Complete Example Session
// ============================================
// PART 1: THE PLUGIN MODULE
// ============================================
// my-plugin.js
import { doc } from 'prettier';
const { group, indent, line, softline, hardline, concat } = doc.builders;
// ============================================
// PART 2: THE LANGUAGES ARRAY
// ============================================
export const languages = [
{
name: 'MyLanguage',
parsers: ['my-parser'],
extensions: ['.mylang'],
},
];
// ============================================
// PART 3: THE PARSER
// ============================================
export const parsers = {
'my-parser': {
parse: (text) => {
// The parse function returns an AST.
// The AST must have type and location on every node.
const ast = {
type: 'Program',
start: 0,
end: text.length,
body: [],
};
// Parse the text and populate the body.
return ast;
},
locStart: (node) => node.start,
locEnd: (node) => node.end,
},
};
// ============================================
// PART 4: THE PRINTER
// ============================================
export const printers = {
'my-ast': {
print: (path, options, print) => {
const node = path.getValue();
switch (node.type) {
case 'Program':
return concat([
path.map(print, 'body'),
]);
case 'VariableDeclaration':
return group(concat([
node.kind,
' ',
path.call(print, 'id'),
' = ',
path.call(print, 'init'),
';',
]));
case 'Identifier':
return node.name;
case 'Literal':
return JSON.stringify(node.value);
default:
return '';
}
},
},
};
// ============================================
// PART 5: THE CONFIGURATION
// ============================================
// .prettierrc
// {
// "plugins": ["./my-plugin.js"]
// }
// The plugin is registered by adding it to the plugins array.
// ============================================
// PART 6: THE CLI USAGE
// ============================================
// npx prettier src/file.mylang --write
//
// Prettier detects the .mylang extension.
// It uses the my-parser parser.
// It uses the my-ast printer.
// It writes the formatted output.
// ============================================
// PART 7: THE ASYNC PARSER
// ============================================
// Prettier 3.0 supports async parsers.
export const parsers = {
'my-parser': {
parse: async (text) => {
const ast = await parseMyLanguageAsync(text);
return ast;
},
locStart: (node) => node.start,
locEnd: (node) => node.end,
},
};
// ============================================
// PART 8: THE COMMENT ATTACHMENT
// ============================================
// The default comment attachment uses locStart and locEnd.
// The plugin can override it with canAttachComment.
export const printers = {
'my-ast': {
canAttachComment: (node) => {
return node.type !== 'Comment';
},
printComment: (path) => {
const node = path.getValue();
return node.value;
},
},
};
// ============================================
// PART 9: THE PRAGMA INSERTION
// ============================================
// The plugin can implement the --insert-pragma behavior.
export const printers = {
'my-ast': {
insertPragma: (text) => {
return `/** @format */\n\n${text}`;
},
},
};
// ============================================
// PART 10: THE VERIFICATION CHECKLIST
// ============================================
// [ ] The plugin exports languages, parsers, and printers.
// [ ] The languages array declares the extensions.
// [ ] The parse function returns an AST with type and locations.
// [ ] The locStart and locEnd functions return offsets.
// [ ] The print function returns a Doc, not a string.
// [ ] The plugin is registered in the Prettier configuration.
// [ ] The CLI usage works with the custom extension.
The ten parts covered the plugin module, the languages array, the parser, the printer, the configuration, the CLI usage, the async parser, the comment attachment, the pragma insertion, and the verification. The through-line is that the plugin is a module with three exports, and the parser is the half that produces the AST.
Quick Reference
The plugin exports
| Export | Purpose |
|---|---|
languages | Declares the file extensions and parsers |
parsers | Contains the parse, locStart, and locEnd functions |
printers | Contains the print function and the comment hooks |
The parser functions
| Function | Signature | Purpose |
|---|---|---|
parse | (text, options) => AST | Parses the source to an AST |
locStart | (node) => number | Returns the node’s start offset |
locEnd | (node) => number | Returns the node’s end offset |
The printer functions
| Function | Purpose |
|---|---|
print | Returns a Doc for the node |
canAttachComment | Decides whether a comment can attach to a node |
printComment | Returns a Doc for a comment node |
insertPragma | Inserts the @format marker |
The document builders
| Builder | Purpose |
|---|---|
group | Prints on one line if it fits, breaks otherwise |
line | Space, or newline when the group breaks |
softline | Nothing, or newline when the group breaks |
hardline | Always a newline |
indent | Increases the indentation |
Best Practices
โ Do This:
// Return a Doc from the print function
return group(concat([node.kind, ' ', path.call(print, 'id')])); // โ
// Provide locStart and locEnd
locStart: (node) => node.start, locEnd: (node) => node.end // โ
// Use the doc builders for the layout
group(concat([/* ... */])) // โ
// Register the plugin in the configuration
"plugins": ["./my-plugin.js"] // โ
โ Don’t Do This:
// Return a string from the print function
return "const x = 1;"; // โ
// Omit the location functions
// The comment attachment and range formatting fail. // โ
// Use the default print for an unknown node
return ''; // the node is silently dropped // โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The plugin is not used | The plugin is not in the plugins array | Add it to the configuration |
| The formatting ignores the print width | The print returns a string | Return a Doc with group and line |
| Comments are printed in the wrong place | The locStart/locEnd are wrong | Return the correct offsets |
| The parser fails on async input | The parse is synchronous | Return a Promise (Prettier 3.0+) |
Real-World Examples
1. The languages array
export const languages = [{ name: 'MyLanguage', parsers: ['my-parser'], extensions: ['.mylang'] }];
2. The parser
export const parsers = { 'my-parser': { parse: (text) => parseMyLanguage(text), locStart: (n) => n.start, locEnd: (n) => n.end } };
3. The printer
export const printers = { 'my-ast': { print: (path, options, print) => printMyNode(path.getValue(), path, options, print) } };
4. The group builder
group(concat([node.kind, ' ', path.call(print, 'id')]));
5. The line builder
concat([node.a, line, node.b]);
6. The async parser
parse: async (text) => await parseMyLanguageAsync(text);
7. The comment hook
canAttachComment: (node) => node.type !== 'Comment';
8. The pragma insertion
insertPragma: (text) => `/** @format */\n\n${text}`;
9. The configuration
{ "plugins": ["./my-plugin.js"] }
10. The CLI usage
npx prettier src/file.mylang --write
Visual
The Plugin Structure
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE PLUGIN MODULE โ
โ โ
โ languages: [{ name, parsers, extensions }] โ
โ โ โ
โ โผ โ
โ parsers: { โ
โ 'my-parser': { โ
โ parse: (text) => AST, โ
โ locStart: (node) => number, โ
โ locEnd: (node) => number, โ
โ } โ
โ } โ
โ โ โ
โ โผ โ
โ printers: { โ
โ 'my-ast': { โ
โ print: (path) => Doc, โ
โ } โ
โ } โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Parser and the Printer
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ FROM SOURCE TO DOC โ
โ โ
โ Source text โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ parse(text) โ Returns an AST โ
โ โ โ with type and locations โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ print(path) โ Returns a Doc โ
โ โ โ (group, line, indent) โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Prettier renderer โ Renders the Doc โ
โ โ โ at the print width โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Doc Builders
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE DOC IS A LAYOUT DESCRIPTION โ
โ โ
โ group(concat([ โ
โ node.kind, // "const" โ
โ " ", โ
โ path.call(print, "id"), // "x" โ
โ " = ", โ
โ path.call(print, "init"), // "1" โ
โ ";" โ
โ ])) โ
โ โ
โ The group tells Prettier: print on one line if it fits. โ
โ The line tells Prettier: this is where the break goes. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Plugin exports | languages, parsers, printers |
| Parser functions | parse, locStart, locEnd |
| Printer function | print (returns a Doc) |
| Comment hooks | canAttachComment, printComment |
| Pragma hook | insertPragma |
| Async support | Prettier 3.0+ |
| Registration | plugins array in the configuration |
Key takeaways:
- A Prettier plugin exports
languages,parsers, andprinters. Thelanguagesarray declares the file extensions and the parser names. Theparsersobject contains theparsefunction and thelocStart/locEndfunctions. Theprintersobject contains theprintfunction that returns aDoc. - The parser must produce an AST with accurate locations. The
locStartandlocEndfunctions are used for comment attachment, range formatting, and cursor offset. A parser that omits the locations produces a plugin that fails on those features. - The printer must return a
Doc, not a string. TheDocis built with thedoc.buildershelpersโgroup,line,softline,hardline,indent,concat. The renderer decides the line breaks based on theprintWidthoption . - The plugin can override the comment algorithm. The
canAttachComment,printComment, andhandleCommentshooks are the mechanism for a language whose comments do not fit the default attachment algorithm . - Prettier 3.0 supports async parsers. The
parsefunction can return aPromisethat resolves to the AST. The async support is the mechanism for a plugin that spawns a process or makes a network request .
Remember: A Prettier plugin is a module with three exports. The parser turns the source into an AST, and the printer turns the AST into a document. The document is the layout description, and Prettier’s renderer decides the line breaks. The parser must provide the locations, and the printer must return a Doc. The plugin is registered by adding it to the plugins array. The custom parser is the first half of the plugin, and the custom printer is the second. The two halves are connected by the AST, and the AST is the contract.
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!