跳至主内容区

Prettier 1.3 发布

· 1 分钟阅读
非官方测试版翻译

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

本文介绍了 Facebook 采用 Prettier 的最新进展,汇报了 TypeScript 支持工作的当前状态,并详细说明了 Prettier 1.3.0 版本包含的各项改进与修复。

Facebook 采用进展

我 (@vjeux) 投入开发 Prettier 的初衷始终是为了让整个 Facebook 代码库完成迁移。在此我想同步当前进展以及实现这一目标的推进方案。

最早采用 Prettier 的项目是 Jest、React 和 immutable-js。这些代码库规模较小(约数百个文件),拥有独立的基础设施,并由少数全职开发者维护。

随后,Oculus 和 Nuclide 也迁移了他们的代码库。虽然规模更大(数千个文件,数十名全职贡献者),但迁移方式与首批项目类似:通过一次性大型 codemod 完成转换。

然而整个 Facebook 代码库的规模远超前述项目,无法一次性完成迁移,也难以说服所有开发者接受代码库在开发过程中被全面重格式化。因此我们需要更渐进式的推进方案。

规模化采用方案

对代码执行 Prettier 格式化是开销较大的操作:它会让你的 pull request 因大量无关变更而显得混乱,并导致所有进行中的 pull request 出现合并冲突。因此一旦文件完成格式化,就必须确保其保持格式化状态

  • 美化文件时,在首个块注释(如 @flow)处添加 @format 标记

  • 建立带自动修复功能的 lint 规则,检查含 @format 标记的文件是否格式正确

    • 在 Nuclide 中运行时,会以内联警告形式提示并提供修复按钮
    • 提交 pull request 时,lint 检查将失败并显示 [Yn] 提示,只需按回车即可修复
  • 更新默认代码模板,在文件头添加 @format 标记

  • 在 Nuclide 中通过 cmd-shift-c 执行代码格式化时,自动插入 @format 文件头

  • 当文件头含 @format 标记时,禁用所有 max-len 等格式规则

  • 提供单行命令脚本,支持对整个目录执行 Prettier 格式化

  • 编写完整指南,帮助开发者迁移代码库并提供最佳实践

  • 发布新版 Prettier 时,对所有含 @format 的文件执行格式化,避免后续告警

  • 添加对含 @format 文件数量的长期追踪

我们在 1.5 周前完成上述所有工具链的搭建,反响极其热烈。多个团队的开发者已自主完成代码库迁移。截至目前,Facebook 代码库的 15% 已完成转换!

开始开发 Prettier 时,我就预感开发者亟需代码格式化工具。但完全没想到工具链就绪后,大家会如此积极地迁移代码库!这充分证明该项目确实解决了实际问题,而非华而不实的噱头。

TypeScript 支持进展

@despairblue, @azz@JamesHenry 正全力实现 Prettier 对 TypeScript 的支持(这是需求最高的功能)。目前 TypeScript 测试套件中 11000 个文件尚有 2000 个未完全支持。您可关注 https://github.com/prettier/prettier/issues/1480 了解进展,欢迎随时参与贡献!

Flow

为 Flow 泛型添加尾部逗号 (#1381)

--trailing-comma=all 选项本应在所有可能位置添加尾部逗号,但我们疏忽了在 Flow 泛型中的实现。

// Before
type Errors = Immutable.Map<
Ahohohhohohohohohohohohohohooh,
Fbt | Immutable.Map<ErrorIndex, Fbt>
>;

// After
type Errors = Immutable.Map<
Ahohohhohohohohohohohohohohooh,
Fbt | Immutable.Map<ErrorIndex, Fbt>,
>;

内联 Flow 泛型中的可为空类型 (#1426)

在确保正确打印代码后,我们开始优化输出以更贴近实际编码风格。内联可选 Flow 类型是提升代码可读性的小改进。

// Before
type Cursor = Promise<
?{
newCursor?: number,
formatted: string,
}
>;

// After
type Cursor = Promise<?{
newCursor?: number,
formatted: string,
}>;

允许 Flow 声明在 StringLiteralTypeAnnotations 处换行 (#1437)

当内容超出 80 列限制时,我们不断寻找更多换行点。本次改进针对常量字符串类型的声明场景。

// Before
export type AdamPlacementValidationSingleErrorKey = 'SOME_FANCY_TARGETS.GLOBAL_TARGET';

// After
export type AdamPlacementValidationSingleErrorKey =
'SOME_FANCY_TARGETS.GLOBAL_TARGET';

在 Flow 泛型默认参数中的 = 周围添加空格 (#1476)

这是优化 Flow 代码显示的又一细节。函数默认参数中我们在 = 两侧添加空格,但泛型中却遗漏了。

// Before
class PolyDefault<T=string> {}

// After
class PolyDefault<T = string> {}

无括号单参数 Flow 函数不换行 (#1452)

这个改动有些特别... 它看起来确实有点奇怪!

// Before
const selectorByPath:
Path
=> SomethingSelector<
SomethingUEditorContextType,
SomethingUEditorContextType,
SomethingBulkValue<string>
> = memoizeWithArgs(/* ... */)

// After
const selectorByPath: Path => SomethingSelector<
SomethingUEditorContextType,
SomethingUEditorContextType,
SomethingBulkValue<string>
> = memoizeWithArgs(/* ... */);

修复 Flow 可选类型括号问题 (#1357)

此前对 Flow 可选类型的括号处理过于宽松。在 Facebook 代码库中发现一处语义变更案例。本次修复严格限定了无需括号的类型范围。

// Before
type X = ?(number, number) => number => void;

// After
type X = (?(number, number) => number) => void;

跳过 FlowShorthandWithOneArg 的尾部逗号 (#1364)

在箭头函数类型参数中,无括号时添加尾部逗号会导致解析错误。我们在 Facebook 代码库中发现过此罕见案例。

// Before
type IdeConnectionFactory =
child_process$ChildProcess,
=> FlowIDEConnection = defaultIDEConnectionFactory;

// After
type IdeConnectionFactory =
child_process$ChildProcess
=> FlowIDEConnection = defaultIDEConnectionFactory;

重排 Flow 对象属性 (#1451)

这个案例暴露了 AST 结构的不利影响。常规键和数组键被分为独立组而非统一列表。为恢复原始顺序,我们不得不回退到源码解析 :(

// Before
type Foo = {
[key: string]: void,
alpha: "hello",
beta: 10
};

// After
type Foo = {
alpha: 'hello',
[key: string]: void,
beta: 10
}

模板字面量

修复模板字面量缩进 (#1385)

模板字面量在 ${} 内部的缩进问题由来已久。原采用反引号缩进方案效果不佳,现改为采用 ${ 的缩进级别,神奇地使 GraphQL 查询排版更美观!

// Before
Relay.createContainer({
nodes: ({ solution_type, time_frame }) => Relay.QL`
fragment {
__typename
${OptimalSolutionsSection.getFragment("node", {
solution_type,
time_frame
})}
}
`
})
// After
Relay.createContainer({
nodes: ({ solution_type, time_frame }) => Relay.QL`
fragment {
__typename
${OptimalSolutionsSection.getFragment("node", {
solution_type,
time_frame
})}
}
`
})

单参数模板字面量调用不缩进 (#873)

模板字面量内部空格具有语义,这给排版器带来巨大挑战。面对单参数模板字面量调用,我们曾保持原样处理,但持续收到错误缩进报告。现已改为内联处理,期待覆盖多数场景。

// Before
insertRule(
`*, *:before, *:after {
box-sizing: inherit;
}`,
);

// After
insertRule(`*, *:before, *:after {
box-sizing: inherit;
}`);

修复模板字面量的 Windows 换行符问题 (#1439)

在 Prettier 的许多代码处理环节中,我们精心处理了 \n\r\n 两种换行符,但唯独在模板字符串的处理上有所疏漏。现在这个问题已经修复!

// Before
const aLongString = `

Line 1

Line 2

Line 3

`;

// After
const aLongString = `
Line 1
Line 2
Line 3
`;

将内联模板字符串作为箭头函数体 (#1485)

我们之前已支持带标签的模板字符串内联(例如 graphql`query`),但对普通模板字符串尚未支持。有趣的是,相关代码本应支持此功能,但却错误使用了 TemplateElement 而非 TemplateLiteral :(

// Before
const inlineStore = preloadedState =>
`
<script>
window.preloadedState = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')}
</script>
`

// After
const inlineStore = preloadedState => `
<script>
window.preloadedState = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')}
</script>
`

三元表达式

为非常规嵌套三元表达式添加括号 (#1386)

在处理嵌套三元表达式时,开发者通常聚焦于最常见的 if-then-else 形式(cond1 ? elem1_if : cond2 ? elem2_if : elem_else)。但如果调整 ?: 的位置,会出现语义不同的变体。为避免混淆,我们为这种非常规形式添加了括号。

// Before
cond1 ? cond2 ? elem2_if : elem2_else : elem1_else

// After
cond1 ? (cond2 ? elem2_if : elem2_else) : elem1_else

仅在必要时为箭头函数内的三元表达式添加括号 (#1450)

ESLint 规则 no-confusing-arrows 建议为无花括号的箭头函数中的三元表达式添加括号。以下两种写法确实容易混淆:

var x = a => 1 ? 2 : 3;
var x = a <= 1 ? 2 : 3;

单行代码中添加括号有其意义,但在多行格式下,缩进已能清晰表达结构,此时括号反而多余。因此我们现在仅在需要消除歧义时添加括号。

// Before
var x = a => (1 ? 2 : 3);
var x = a =>
(1
? 2
: 3);

// After
var x = a => (1 ? 2 : 3);
var x = a =>
1
? 2
: 3;

JavaScript 通用改进

内联单参数对象形式的函数声明 (#1173)

此功能常被 React 无状态函数组件(SFC)使用者期待。如果您大量使用这类组件,本次变更可能影响较大。

// Before
const X = (
props: {
a: boolean,
},
) => <div />;

// After
const X = (props: {
a: boolean,
}) => <div />;

优先拆分函数参数中的内联对象 (#1453)

我们早期发现开发者通常会在函数返回类型前拆分参数。但处理单解构参数的内联代码打破了这一惯例,导致出现不美观的格式(如示例所示)。好消息是,此修复使我们能够开启单参数对象类型的自动内联功能。

// Before
class X {
async onDidInsertSuggestion({editor, triggerPosition, suggestion}): Promise<
void
> {
}
}

// After
class X {
async onDidInsertSuggestion({
editor,
triggerPosition,
suggestion
}): Promise<void> {
}
}

空数组/对象不换行 (#1440)

这是个存在已久却易于修复的问题,但具有重要价值:每当用户报告 []{} 被意外换行,我们都能通过修复其他问题来解决。这曾是发现边缘案例的有效途径。幸运的是,这类案例现已穷尽——近期所有类似换行都纯属多余。是时候彻底解决它了!

// Before
const a = someVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeLong.Expression || [
];

// After
const a = someVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeLong.Expression || [];

[0] 索引访问不换行 (#1441)

我们收到大量关于数组访问换行导致格式问题的报告。虽然尚无通用解决方案,但可以针对常见场景 [0] 进行专项优化。

// Before
queryThenMutateDOM(() => {
title = SomeThing.call(root, "someLongStringThatPushesThisTextReall")[
0
];
});

// After
queryThenMutateDOM(() => {
title = SomeThing.call(
root,
"someLongStringThatPushesThisTextReall",
)[0];
});

缩进 do-while 条件语句 (#1373)

此前 do-while 条件语句的缩进处理存在逻辑缺陷,经开发者提醒现已修正!

// Before
do {}
while (someVeryLongStringA && someVeryLongStringB && someVeryLongStringC && someVeryLongStringD);

// After
do {}
while (
someVeryLongStringA &&
someVeryLongStringB &&
someVeryLongStringC &&
someVeryLongStringD
);

保留参数末尾的内联注释 (#1390)

我们在处理JSX属性和函数参数的注释检测逻辑时遗漏了一种情况——当注释出现在最后位置时,会导致注释被错误地放置到闭合符号之后。在JSX场景中,这会产生语义变化的代码。虽然生产环境不受影响(毕竟我们通常不会提交被注释的代码),但在开发过程中这确实是个糟糕的体验。

// Before
const x = (
<div
attr1={1}>
// attr3={3}
{children}
</div>
);

// After
const x = (
<div
attr1={1}
// attr3={3}
>
{children}
</div>
);

修复箭头函数返回类表达式的换行问题 (#1464)

在1.0版本中,我们将箭头函数内部的类表达式设置为内联显示。实践证明当类结构较复杂时这种处理效果不佳,因此我们决定回退这项变更。我们始终尽量避免这种反复变更的草率决策,但偶尔也会允许自己修正错误!

// Before
export default (ViewComponent: Function, ContainerComponent: Function) =>
class extends React.Component {
static propTypes = {};
};

// After
export default (ViewComponent: Function, ContainerComponent: Function) =>
class extends React.Component {
static propTypes = {};
};

修复含空语句代码块中的空行问题 (#1375)

这个漏洞是通过模糊测试发现的。实际编码中虽然很难遇到这种情况,但修复总是好的!

// Input
if (a) {
b;


;
}

// Before
if (a) {
b;

}

// After
if (a) {
b;
}