插件
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
插件是为 Prettier 添加新语言或格式化规则的方式。Prettier 自身对所有语言的实现都是通过插件 API 表达的。核心 prettier 包内置了 JavaScript 及其他面向 Web 的语言支持。如需支持其他语言,你需要安装相应插件。
使用插件
你可以通过以下方式加载插件:
-
通过 CLI 的
--plugin选项:prettier --write main.foo --plugin=prettier-plugin-foo提示你可以多次设置
--plugin选项。 -
通过 API 的
plugins选项:await prettier.format("code", {
parser: "foo",
plugins: ["prettier-plugin-foo"],
}); -
通过 配置文件:
{
"plugins": ["prettier-plugin-foo"]
}
传递给 plugins 的字符串最终会交由 import() 表达式处理,因此你可以提供模块/包名称、路径或任何 import() 支持的参数。
官方插件
社区插件
-
prettier-plugin-apex(由 @dangmai 维护) -
prettier-plugin-elm(由 @giCentre 维护) -
prettier-plugin-glsl(由 @NaridaL 维护) -
prettier-plugin-sh(由 @JounQin 贡献) -
prettier-plugin-sql(由 @JounQin 贡献) -
prettier-plugin-sql-cst(由 @nene 贡献) -
prettier-plugin-toml(由 @JounQin 和 @so1ve 贡献)
开发插件
Prettier 插件是标准的 JavaScript 模块,需要导出以下五个属性,或者通过默认导出包含以下属性的对象:
-
languages -
parsers -
printers -
options -
defaultOptions
languages
Languages 是一个语言定义数组,表示您的插件将为 Prettier 添加的语言支持。它可以包含 prettier.getSupportInfo() 中指定的所有字段。
该数组必须包含 name 和 parsers 字段。
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 格式名称以及两个位置提取函数(locStart 和 locEnd)。
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;
位置提取函数(locStart 和 locEnd)返回给定 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 函数的对象,其余属性(embed、preprocess 等)均为可选。
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;
打印过程包含以下步骤:
-
AST 预处理(可选)。参见
preprocess。 -
注释附加(可选)。参见在打印机中处理注释。
-
处理嵌入式语言(可选)。若定义了
embed方法,会为每个节点(深度优先)调用。出于性能考虑,递归本身是同步的,但embed可返回异步函数(用于调用其他解析器/打印机处理 CSS-in-JS 等嵌入式语法)。这些函数将在下一步前排队顺序执行。 -
递归打印。从 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 同步返回文档(这种方式可能更方便),但此时无法使用 textToDoc 和 print 回调函数。如需使用这些功能,请返回函数形式。
若 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 注释节点标注(即创建新属性)至少以下属性之一:enclosingNode、precedingNode 或 followingNode。这些属性可辅助插件决策(当然也可使用完整 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 属性(注释数组)。每个注释应包含以下属性:leading、trailing、printed。
上面的示例使用了 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;
教程
- 如何为 Prettier 编写插件:教你如何编写一个基础的 Prettier 插件来处理 TOML 格式。
测试插件
由于插件支持通过相对路径解析,在开发过程中你可以这样做:
import * as prettier from "prettier";
const code = "(add 1 2)";
await prettier.format(code, {
parser: "lisp",
plugins: ["./index.js"],
});
这种方式会基于当前工作目录解析插件路径。