跳至主内容区

插件

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

插件是为 Prettier 添加新语言或格式化规则的方式。Prettier 自身对所有语言的实现都是通过插件 API 表达的。核心 prettier 包内置了 JavaScript 及其他面向 Web 的语言支持。如需支持其他语言,你需要安装相应插件。

使用插件

你可以通过以下方式加载插件:

  • 通过 CLI--plugin 选项:

    prettier --write main.foo --plugin=prettier-plugin-foo
    提示

    你可以多次设置 --plugin 选项。

  • 通过 APIplugins 选项:

    await prettier.format("code", {
    parser: "foo",
    plugins: ["prettier-plugin-foo"],
    });
  • 通过 配置文件

    {
    "plugins": ["prettier-plugin-foo"]
    }

传递给 plugins 的字符串最终会交由 import() 表达式处理,因此你可以提供模块/包名称、路径或任何 import() 支持的参数。

官方插件

社区插件

开发插件

Prettier 插件是标准的 JavaScript 模块,需要导出以下五个属性,或者通过默认导出包含以下属性的对象:

  • languages

  • parsers

  • printers

  • options

  • defaultOptions

languages

Languages 是一个语言定义数组,表示您的插件将为 Prettier 添加的语言支持。它可以包含 prettier.getSupportInfo() 中指定的所有字段。

该数组必须包含 nameparsers 字段。

export const languages = [
{
// The language name
name: "InterpretedDanceScript",
// Parsers that can parse this language.
// This can be built-in parsers, or parsers you have contributed via this plugin.
parsers: ["dance-parse"],
},
];

parsers

解析器(parsers)负责将字符串形式的代码转换为抽象语法树(AST)

解析器的键名必须与 languages 数组中 parsers 字段指定的名称匹配。其值包含解析函数、AST 格式名称以及两个位置提取函数(locStartlocEnd)。

export const parsers = {
"dance-parse": {
parse,
// The name of the AST that the parser produces.
astFormat: "dance-ast",
hasPragma,
hasIgnorePragma,
locStart,
locEnd,
preprocess,
},
};

parse 函数的签名如下:

function parse(text: string, options: object): Promise<AST> | AST;

位置提取函数(locStartlocEnd)返回给定 AST 节点的起始和结束位置:

function locStart(node: object): number;

(可选)pragma 检测函数(hasPragma)应返回文本是否包含 pragma 注释。

function hasPragma(text: string): boolean;

(可选)“忽略 pragma”检测函数(hasIgnorePragma)应返回文本是否包含指示不应格式化的 pragma。

function hasIgnorePragma(text: string): boolean;

(可选)预处理函数可在传入 parse 函数前处理输入文本。

function preprocess(text: string, options: object): string | Promise<string>;

对异步预处理的支持首次添加于 v3.7.0 版本

printers

打印机(printers)将 AST 转换为 Prettier 的中间表示形式(称为 Doc)。

键名必须与解析器生成的 astFormat 匹配。值应包含带 print 函数的对象,其余属性(embedpreprocess 等)均为可选。

export const printers = {
"dance-ast": {
print,
embed,
preprocess,
getVisitorKeys,
insertPragma,
canAttachComment,
isBlockComment,
printComment,
getCommentChildNodes,
hasPrettierIgnore,
printPrettierIgnored,
handleComments: {
ownLine,
endOfLine,
remaining,
},
},
};

打印过程

Prettier 使用称为 Doc 的中间表示形式(根据 printWidth 等选项转换为字符串)。_打印机_的任务是获取 parsers[<parser name>].parse 生成的 AST 并返回 Doc。Doc 通过构建器命令构建:

import * as prettier from "prettier";

const { join, line, ifBreak, group } = prettier.doc.builders;

打印过程包含以下步骤:

  1. AST 预处理(可选)。参见 preprocess

  2. 注释附加(可选)。参见在打印机中处理注释

  3. 处理嵌入式语言(可选)。若定义了 embed 方法,会为每个节点(深度优先)调用。出于性能考虑,递归本身是同步的,但 embed 可返回异步函数(用于调用其他解析器/打印机处理 CSS-in-JS 等嵌入式语法)。这些函数将在下一步前排队顺序执行。

  4. 递归打印。从 AST 递归构建 Doc:

    • 若步骤 3 存在当前节点的嵌入式语言 Doc,则使用该 Doc
    • 否则调用 print(path, options, print): Doc 方法(通常通过 print 回调打印子节点来组合当前节点的 Doc)

print

插件打印机的主要工作在 print 函数中完成,其签名为:

function print(
// Path to the AST node to print
path: AstPath,
options: object,
// Recursively print a child node
print: (selector?: string | number | Array<string | number> | AstPath) => Doc,
): Doc;

print 函数接收以下参数:

  • path:用于访问 AST 节点的对象(类堆栈数据结构,维护递归状态)。称为 "path" 因其表示从 AST 根节点到当前节点的路径,当前节点可通过 path.node 获取。

  • options:包含全局配置的持久化对象,插件可修改它以存储上下文数据。

  • print:打印子节点的回调函数。该函数包含核心打印逻辑(具体步骤由插件实现),会调用打印机的 print 函数并传递自身。因此这两个 print 函数——一个来自核心,一个来自插件——在递归遍历 AST 时会相互调用。

以下简化示例展示典型 print 实现:

import * as prettier from "prettier";

const { group, indent, join, line, softline } = prettier.doc.builders;

function print(path, options, print) {
const node = path.node;

switch (node.type) {
case "list":
return group([
"(",
indent([softline, join(line, path.map(print, "elements"))]),
softline,
")",
]);

case "pair":
return group([
"(",
indent([softline, print("left"), line, ". ", print("right")]),
softline,
")",
]);

case "symbol":
return node.name;
}

throw new Error(`Unknown node type: ${node.type}`);
}

查看 prettier-python 的打印机实现获取更多范例。

(可选)embed

打印机可以通过 embed 方法实现在一种语言内部打印另一种语言。典型的应用场景包括在 JS 中打印 CSS(CSS-in-JS)或在 Markdown 中打印围栏代码块。其函数签名为:

function embed(
// Path to the current AST node
path: AstPath,
// Current options
options: Options,
):
| ((
// Parses and prints the passed text using a different parser.
// You should set `options.parser` to specify which parser to use.
textToDoc: (text: string, options: Options) => Promise<Doc>,
// Prints the current node or its descendant node with the current printer
print: (
selector?: string | number | Array<string | number> | AstPath,
) => Doc,
// The following two arguments are passed for convenience.
// They're the same `path` and `options` that are passed to `embed`.
path: AstPath,
options: Options,
) => Promise<Doc | undefined> | Doc | undefined)
| Doc
| undefined;

embed 方法与 print 方法类似,都是将 AST 节点映射为文档(doc),但与 print 不同的是,它能够通过返回异步函数执行异步操作。该异步函数的第一个参数 textToDoc 是异步函数,可用于调用其他插件来渲染文档。

如果 embed 返回的函数直接返回文档(doc)或解析为文档的 Promise,则该文档将用于打印输出,且不会为该节点调用 print 方法。在极少数情况下,也可以直接从 embed 同步返回文档(这种方式可能更方便),但此时无法使用 textToDocprint 回调函数。如需使用这些功能,请返回函数形式。

embed 返回 undefined,或其返回的函数返回 undefined 或解析为 undefined 的 Promise,则该节点将通过 print 方法正常打印。如果返回的函数抛出错误或返回被拒绝的 Promise(例如发生解析错误时),也将触发相同行为。若需要 Prettier 重新抛出这些错误,请将 PRETTIER_DEBUG 环境变量设为非空值。

例如,处理嵌入式 JavaScript 节点的插件可能包含如下 embed 方法:

function embed(path, options) {
const node = path.node;
if (node.type === "javascript") {
return async (textToDoc) => {
return [
"<script>",
hardline,
await textToDoc(node.javaScriptCode, { parser: "babel" }),
hardline,
"</script>",
];
};
}
}

--embedded-language-formatting 选项设为 off 时,将完全跳过嵌入处理步骤:不调用 embed 方法,所有节点都通过 print 方法打印。

(可选)preprocess

preprocess 方法可在 AST 传递给 print 方法前对其进行预处理。

function preprocess(ast: AST, options: Options): AST | Promise<AST>;

(可选)getVisitorKeys

当插件涉及注释附着或嵌入式语言功能时,此属性尤为重要。这些功能需要从根节点开始遍历 AST 的所有自有可枚举属性。若 AST 存在循环引用,遍历将陷入无限循环。此外,节点可能包含非节点对象(如位置数据),遍历这些对象会造成资源浪费。为解决这些问题,打印机可定义返回应遍历属性名的函数。

其函数签名为:

function getVisitorKeys(node, nonTraversableKeys: Set<string>): string[];

默认的 getVisitorKeys 实现:

function getVisitorKeys(node, nonTraversableKeys) {
return Object.keys(node).filter((key) => !nonTraversableKeys.has(key));
}

第二个参数 nonTraversableKeys 是包含常见键名及 Prettier 内部使用键名的集合。

若已明确所有访问键名:

const visitorKeys = {
Program: ["body"],
Identifier: [],
// ...
};

function getVisitorKeys(node /* , nonTraversableKeys*/) {
// Return `[]` for unknown node to prevent Prettier fallback to use `Object.keys()`
return visitorKeys[node.type] ?? [];
}

若仅需排除少量键名:

const ignoredKeys = new Set(["prev", "next", "range"]);

function getVisitorKeys(node, nonTraversableKeys) {
return Object.keys(node).filter(
(key) => !nonTraversableKeys.has(key) && !ignoredKeys.has(key),
);
}

(可选)insertPragma

插件可通过 insertPragma 函数实现使用 --insert-pragma 选项时,如何在结果代码中插入编译指示(pragma)注释。其函数签名为:

function insertPragma(text: string): string;

打印机中的注释处理

注释通常不属于语言的 AST,这给打印机带来特殊挑战。Prettier 插件可选择在 print 函数中自行处理注释,或依赖 Prettier 的注释算法。

默认情况下,若 AST 存在顶层 comments 属性,Prettier 会认为 comments 存储注释节点数组,并通过 parsers[<plugin>].locStart/locEnd 函数定位注释所属的 AST 节点。注释将被附加到对应节点(此过程会改变 AST),同时从根节点删除 comments 属性。*Comment 函数用于调整 Prettier 的算法。注释附加完成后,Prettier 将自动调用 printComment(path, options): Doc 函数,并将返回文档插入预期位置。

(可选)getCommentChildNodes

默认情况下,Prettier 会递归搜索每个节点的所有对象属性(除少数预定义属性外)。可通过提供此函数覆盖默认行为,其函数签名为:

function getCommentChildNodes(
// The node whose children should be returned.
node: AST,
// Current options
options: object,
): AST[] | undefined;

若节点无子节点则返回空数组 [],返回 undefined 则回退至默认行为。

(可选)hasPrettierIgnore

function hasPrettierIgnore(path: AstPath): boolean;

返回 AST 节点是否被 prettier-ignore 忽略。

(可选)printPrettierIgnored

如果 AST 节点被 prettier-ignore 忽略,默认情况下 Prettier 会截取文本进行解析而不调用 print 函数。但插件也可以通过添加此属性来处理被 prettier-ignore 忽略节点的打印。

此属性与 print 属性具有相同的函数签名。

首次出现于 v3.7.0 版本

####(可选)printComment

当需要打印注释节点时调用,其函数签名为:

function printComment(
// Path to the current comment node
commentPath: AstPath,
// Current options
options: object,
): Doc;

####(可选)canAttachComment

function canAttachComment(node: AST, ancestors: T[]): boolean;

此函数用于判定注释是否可附加到特定 AST 节点。默认会遍历 所有 AST 属性以寻找可附加注释的节点,此函数可阻止注释附加到特定节点。典型实现示例如下:

function canAttachComment(node, [parent]) {
return !(
node.type === "comment" ||
(parent?.type === "shorthand-property" &&
parent.key === node &&
parent.key !== parent.value)
);
}

第二个参数 ancestors 首次添加于 v3.7.0 版本。

####(可选)isBlockComment

function isBlockComment(node: AST): boolean;

返回 AST 节点是否为块注释。

####(可选)handleComments

handleComments 对象包含三个可选函数,其函数签名均为:

(
// The AST node corresponding to the comment
comment: AST,
// The full source code text
text: string,
// The global options object
options: object,
// The AST
ast: AST,
// Whether this comment is the last comment
isLastComment: boolean,
) => boolean;

这些函数用于覆盖 Prettier 的默认注释附加算法。ownLine/endOfLine/remaining 函数应手动将注释附加到节点并返回 true,或返回 false 交由 Prettier 处理。

根据注释节点周围的文本,Prettier 会调用:

  • ownLine:当注释前仅有空白符且后跟换行符时

  • endOfLine:当注释后跟换行符但前面存在非空白符时

  • remaining:其余所有情况

调度时,Prettier 已为每个 AST 注释节点标注(即创建新属性)至少以下属性之一:enclosingNodeprecedingNodefollowingNode。这些属性可辅助插件决策(当然也可使用完整 AST 和原始文本进行复杂判断)。

手动附加注释

prettier.util.addTrailingComment/addLeadingComment/addDanglingComment 函数可用于手动将注释附加到 AST 节点。以下是一个示例 ownLine 函数(为演示目的虚构),用于确保注释不会跟在"标点"节点之后:

import * as prettier from "prettier";

function ownLine(comment, text, options, ast, isLastComment) {
const { precedingNode } = comment;
if (precedingNode && precedingNode.type === "punctuation") {
prettier.util.addTrailingComment(precedingNode, comment);
return true;
}
return false;
}

带注释的节点需包含 comments 属性(注释数组)。每个注释应包含以下属性:leadingtrailingprinted

上面的示例使用了 prettier.util.addTrailingComment,该函数会自动将 comment.leading/trailing/printed 设置为适当的值,并将注释添加到 AST 节点的 comments 数组中。

--debug-print-comments CLI 标志可辅助调试注释附加问题。该标志会输出详细的注释分类信息,包括:注释类型(ownLine/endOfLine/remaining)、附加方式(leading/trailing/dangling)及附加节点。对于 Prettier 内置语言,此信息也可在 Playground 的调试区域通过勾选"显示注释"查看。

options

options 是包含插件支持的自定义选项的对象。

示例:

export default {
// ... plugin implementation
options: {
openingBraceNewLine: {
type: "boolean",
category: "Global",
default: true,
description: "Move open brace for code blocks onto new line.",
},
},
};

defaultOptions

若插件需要覆盖 Prettier 核心选项的默认值,可在 defaultOptions 中指定:

export default {
// ... plugin implementation
defaultOptions: {
tabWidth: 4,
},
};

工具函数

prettier.util 为插件提供了以下有限的工具函数集:

type Quote = '"' | "'";
type SkipOptions = { backwards?: boolean };

function getMaxContinuousCount(text: string, searchString: string): number;

function getStringWidth(text: string): number;

function getAlignmentSize(
text: string,
tabWidth: number,
startIndex?: number,
): number;

function getIndentSize(value: string, tabWidth: number): number;

function skip(
characters: string | RegExp,
): (
text: string,
startIndex: number | false,
options?: SkipOptions,
) => number | false;

function skipWhitespace(
text: string,
startIndex: number | false,
options?: SkipOptions,
): number | false;

function skipSpaces(
text: string,
startIndex: number | false,
options?: SkipOptions,
): number | false;

function skipToLineEnd(
text: string,
startIndex: number | false,
options?: SkipOptions,
): number | false;

function skipEverythingButNewLine(
text: string,
startIndex: number | false,
options?: SkipOptions,
): number | false;

function skipInlineComment(
text: string,
startIndex: number | false,
): number | false;

function skipTrailingComment(
text: string,
startIndex: number | false,
): number | false;

function skipNewline(
text: string,
startIndex: number | false,
options?: SkipOptions,
): number | false;

function hasNewline(
text: string,
startIndex: number,
options?: SkipOptions,
): boolean;

function hasNewlineInRange(
text: string,
startIndex: number,
startIndex: number,
): boolean;

function hasSpaces(
text: string,
startIndex: number,
options?: SkipOptions,
): boolean;

function getPreferredQuote(
text: string,
preferredQuoteOrPreferSingleQuote: Quote | boolean,
): Quote;

function makeString(
rawText: string,
enclosingQuote: Quote,
unescapeUnnecessaryEscapes?: boolean,
): string;

function getNextNonSpaceNonCommentCharacter(
text: string,
startIndex: number,
): string;

function getNextNonSpaceNonCommentCharacterIndex(
text: string,
startIndex: number,
): number | false;

function isNextLineEmpty(text: string, startIndex: number): boolean;

function isPreviousLineEmpty(text: string, startIndex: number): boolean;

教程

测试插件

由于插件支持通过相对路径解析,在开发过程中你可以这样做:

import * as prettier from "prettier";

const code = "(add 1 2)";
await prettier.format(code, {
parser: "lisp",
plugins: ["./index.js"],
});

这种方式会基于当前工作目录解析插件路径。