| | |

Prettier 25 ๐ŸŽจ Plugin Architecture โ€” Extending Prettier with custom parsers

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

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

ExportPurpose
languagesDeclares the file extensions and parsers
parsersContains the parse, locStart, and locEnd functions
printersContains the print function and the comment hooks

The parser functions

FunctionSignaturePurpose
parse(text, options) => ASTParses the source to an AST
locStart(node) => numberReturns the node’s start offset
locEnd(node) => numberReturns the node’s end offset

The printer functions

FunctionPurpose
printReturns a Doc for the node
canAttachCommentDecides whether a comment can attach to a node
printCommentReturns a Doc for a comment node
insertPragmaInserts the @format marker

The document builders

BuilderPurpose
groupPrints on one line if it fits, breaks otherwise
lineSpace, or newline when the group breaks
softlineNothing, or newline when the group breaks
hardlineAlways a newline
indentIncreases 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

PitfallWhy It HappensFix
The plugin is not usedThe plugin is not in the plugins arrayAdd it to the configuration
The formatting ignores the print widthThe print returns a stringReturn a Doc with group and line
Comments are printed in the wrong placeThe locStart/locEnd are wrongReturn the correct offsets
The parser fails on async inputThe parse is synchronousReturn 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

ItemValue
Plugin exportslanguages, parsers, printers
Parser functionsparse, locStart, locEnd
Printer functionprint (returns a Doc)
Comment hookscanAttachComment, printComment
Pragma hookinsertPragma
Async supportPrettier 3.0+
Registrationplugins array in the configuration

Key takeaways:

  • A Prettier plugin exports languages, parsers, and printers. The languages array declares the file extensions and the parser names. The parsers object contains the parse function and the locStart/locEnd functions. The printers object contains the print function that returns a Doc .
  • The parser must produce an AST with accurate locations. The locStart and locEnd functions 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. The Doc is built with the doc.builders helpersโ€”group, line, softline, hardline, indent, concat. The renderer decides the line breaks based on the printWidth option .
  • The plugin can override the comment algorithm. The canAttachComment, printComment, and handleComments hooks are the mechanism for a language whose comments do not fit the default attachment algorithm .
  • Prettier 3.0 supports async parsers. The parse function can return a Promise that 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.

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!