跳至主内容区

Prettier 1.4:TypeScript 与 CSS 支持

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

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

本次发布为 Prettier 引入了 TypeScript、CSS、Less 和 SCSS 语言支持!

prettier-revolution-conf

TypeScript 支持

这是 Prettier 用户呼声最高的功能。现在通过 1.4.0 版本,您可以使用 Prettier 格式化 .ts.tsx 文件了!

Prettier 的工作原理是利用解析器生成代码的 AST 表示并重新输出。Babylon(支持 Babel 的解析器)和 Flow 在生成 JavaScript 部分的 AST 时基本遵循 estree 格式,同时为 Flow 特有的语法创建特殊节点。

与 Flow 类似,TypeScript 为其特有语法引入了特殊节点。遗憾的是,它在其余 JavaScript 语法部分并未遵循 estree 格式。这给 Prettier 带来了挑战,因为我们需要几乎完全分叉代码才能支持 TypeScript 的格式化。

AST 的兼容性问题并非新挑战,ESLint 项目也曾受此困扰。由于 AST 结构不同,所有 ESLint 规则都无法直接工作。幸运的是,@JamesHenry@soda0289 开发了 typescript-eslint-parser 项目,它能将 TypeScript AST 转换为 estree 格式——这正是 Prettier 所需的解决方案!

该项目集成至 Prettier 后,@azz@despairblue@Pajn 实现了所有 TypeScript 专用节点,并确保 TypeScript 测试套件的 13k 用例全部通过。这项艰巨工作终于完成,功能已可供使用 :)

我们在 GitHub 上最大的 TypeScript 项目中测试了 Prettier,确保其正确输出代码。目前尚未深入优化代码格式化效果,如发现异常情况,请提交 issue 反馈!

CSS、Less 与 SCSS 支持

虽然 TypeScript 是开源社区最期待的功能,但 CSS 支持却是 Facebook 工程师最迫切的需求。当您习惯在某语言中使用代码美化工具后,自然会希望在所有地方都实现自动化!

CSS 语言的复杂度远低于 JavaScript,其支持实现仅耗时数日。我们采用 @ai 开发的 postcss 作为底层解析器,它支持解析 CSS、Less 和 SCSS。同时我们还依赖以下解析器:

当前 postcss 暂不支持 Sass 或 Stylus 解析。若有开发者愿意实现相关打印功能,我们很乐意提供支持。

请注意:当前 Prettier 仅进行基础代码格式化,尚未支持类似 JavaScript 的 singleQuote 等选项,也未实现颜色值或数字的标准化处理。

编辑器集成

项目第一阶段的目标是确保 Prettier 输出正确且美观的代码。现在基础功能已趋完善,我们将着力优化编辑器集成体验。最新版本支持两大实用特性:保持光标位置稳定,以及支持局部格式化(而非全文件处理)。

请注意,我们刚刚在 Prettier 核心代码中实现了这些功能支持,目前还没有任何编辑器集成使用它们。此外,我们尚未在实际使用场景中充分测试,因此很可能还需要修复一些粗糙的边缘问题!

新增 cursorOffset 选项用于光标位置转换(#1637)by @josephfrazier

目前我们让编辑器自行判断光标应该位于何处,虽然它们做得还不错。但既然我们负责代码的打印输出,我们就能提供更精确的位置信息!

新增 --range-start/end 选项以仅格式化部分代码(#1609)by @josephfrazier

这是用户呼声极高的功能。当前 Prettier 只能格式化整个文件,现在则可以选择格式化特定代码段了。

其实现原理是通过向上遍历 AST 来找到最近的完整语句。这意味着你无需精确选择有效范围——只需大致框选想要重新格式化的代码区域,它就能智能处理!

新增 filepath 选项以支持文件类型推断(#1835)by @mitermayer

由于现在支持 CSS 和 TypeScript 格式化,为每个文件指定解析器变得很不方便。现在只需传入文件路径,Prettier 就能通过扩展名自动选择正确的解析器。

重要更新

JSX 内部文本内容自动换行(#1120, #1671, #1827, #1829)by @karl

用户使用 Prettier 处理 JSX 时最大的痛点就是文本打印问题。旧版会在文本前添加难看的 {" "},且对超长文本不做处理。现在我们将每个单词视为独立标记,实现了自然的文本流式排版。

这是 @karl 完成的杰出工作——他不仅实现了功能,还在 Prettier 内部引入了新的基础原语,能够智能处理元素序列的打印并在触及边界时自动换行。

// Before
<div>
Please state your
{" "}
<b>name</b>
{" "}
and
{" "}
<b>occupation</b>
{" "}
for the board of directors.
</div>

// After
<div>
Please state your <b>name</b> and <b>occupation</b> for the board of
directors.
</div>

移除箭头函数返回 JSX 时的括号(#1733)by @xixixao

编写函数式组件的开发者会为此欣喜——我们不再为返回 JSX 的箭头函数添加额外括号。

// Before
const render1 = ({ styles }) => (
<div style={styles}>
Keep the wrapping parens. Put each key on its own line.
</div>
);

// After
const render1 = ({ styles }) =>
<div style={styles}>
Keep the wrapping parens. Put each key on its own line.
</div>;

改进模板字面量打印(#1664, #1714)by @josephfrazier

模板字面量的打印始终是 Prettier 的难点。1.3.0 版本已显著改进,而本次更新后,我们相信已妥善处理了所有常见场景。

我们曾添加移除空行的工具来解决某些问题,但这偶尔会导致怪异结果,现已移除。另一项优化是:不再在 ${ 起始处缩进,而是在包含 ${ 的整行起始处进行缩进。

如果此版本后您对模板字符串的输出仍有疑问,请随时告知我们!

// Before
const Bar = styled.div`
color: ${props => (props.highlight.length > 0 ? palette([
'text',
'dark',
'tertiary'
])(props) : palette([
'text',
'dark',
'primary'
])(props))} !important;
`

// After
const Bar = styled.div`
color: ${props =>
props.highlight.length > 0
? palette(["text", "dark", "tertiary"])(props)
: palette(["text", "dark", "primary"])(props)} !important;
`

对赋值和对象值采用相同的换行规则 (#1721)

我们为赋值后换行(如 a = ...)精心设计了大量逻辑。现在这些规则也应用于对象值,将显著改善多行布尔逻辑和大规模条件表达式的格式。这也是我们创建统一打印器的优秀实践案例。

// Before
const o = {
somethingThatsAReallyLongPropName: this.props.cardType ===
AwesomizerCardEnum.SEEFIRST,
};

// After
const o = {
somethingThatsAReallyLongPropName:
this.props.cardType === AwesomizerCardEnum.SEEFIRST,
};

在 !() 内部缩进条件表达式 (#1731)

此前持续有用户反馈其渲染方式问题,我们将其列为"可能较难解决,稍后处理"的事项。结果发现实现起来非常简单,现在就来满足大家的需求!

// Before
const anyTestFailures = !(aggregatedResults.numFailedTests === 0 &&
aggregatedResults.numRuntimeErrorTestSuites === 0);

// After
const anyTestFailures = !(
aggregatedResults.numFailedTests === 0 &&
aggregatedResults.numRuntimeErrorTestSuites === 0
);

格式化修复

尽可能将循环体放在同一行 (#1498)

我们已为 if 语句实现此特性,为保持一致性,现在循环语句也采用相同处理方式。

// Before
for (a in b)
var c = {};

// After
for (a in b) var c = {};

修复 Flow Union 类型的空行问题 (#1511) by @existentialism

我们不应该进行双重缩进 ;)

// Before
type Foo = Promise<

| { ok: true, bar: string, baz: SomeOtherLongType }
| { ok: false, bar: SomeOtherLongType }
>;

// After
type Foo = Promise<
{ ok: true, bar: string, baz: SomeOtherLongType } |
{ ok: false, bar: SomeOtherLongType }
>;

对含行尾注释的单参数不再添加括号 (#1518)

原先检测箭头函数是否省略括号时,仅检查是否存在注释。现在改为只处理行内注释(如 (/* comment */ num)),忽略行尾注释。

// Before
KEYPAD_NUMBERS.map((num) => ( // Buttons 0-9
<div />
));

KEYPAD_NUMBERS.map(num => ( // Buttons 0-9
<div />
));

不再缩进嵌套三元表达式 (#1822)

这避免了看起来像是4字符缩进(实际应为2字符)的问题。虽然多行条件会失去对齐效果,但考虑到嵌套三元表达式的条件通常较简短,我们认为这是更合理的取舍。

// Before
aaaaaaaaaaaaaaa
? bbbbbbbbbbbbbbbbbb
: ccccccccccccccc
? ddddddddddddddd
: eeeeeeeeeeeeeee ? fffffffffffffff : gggggggggggggggg;

// After
aaaaaaaaaaaaaaa
? bbbbbbbbbbbbbbbbbb
: ccccccccccccccc
? ddddddddddddddd
: eeeeeeeeeeeeeee ? fffffffffffffff : gggggggggggggggg;

在 JSX 属性内部内联链式条件表达式 (#1519)

由于后续不可能出现其他内容,我们无需通过缩进来消除歧义。

// Before
<div
src={
!isEnabled &&
diffUpdateMessageInput != null &&
this.state.isUpdateMessageEmpty
}
/>;

// After
<div
src={
!isEnabled &&
diffUpdateMessageInput != null &&
this.state.isUpdateMessageEmpty
}
/>;

取消字符串中不必要的转义字符 (#1575) by @josephfrazier

我们持续优化字符串处理,本次新增功能将移除冗余的反斜杠 \

// Before
a = 'hol\a';

// After
a = 'hola';

修复布尔表达式中空对象的格式 (#1590) by @dmitrika

虽然单独成行的 { 在布尔表达式中显得突兀,但内联空对象会导致异常行为。因此我们保持空对象独立成行。

const x = firstItemWithAVeryLongNameThatKeepsGoing ||
secondItemWithALongNameAsWell || {};

// After
const x =
firstItemWithAVeryLongNameThatKeepsGoing ||
secondItemWithALongNameAsWell ||
{};

移除 For 语句中序列表达式的括号 (#1597) by @k15a

现在 for 循环内多重赋值时不再添加冗余括号。

// Before
for ((i = 0), (len = arr.length); i < len; i++) {

// After
for (i = 0, len = arr.length; i < len; i++) {

当参数有前置注释时不再内联箭头函数 (#1660)

现在箭头函数内部的块注释再也不会导致格式混乱了!

// Before
export const bem = block => /**
* @param {String} [element] - the BEM Element within that block; if undefined, selects the block itself.
*/
element => /**
* @param {?String} [modifier] - the BEM Modifier for the Block or Element; if undefined, selects the Block or Element unmodified.
*/
modifier =>

// After
export const bem = block =>
/**
* @param {String} [element] - the BEM Element within that block; if undefined, selects the block itself.
*/
element =>
/**
* @param {?String} [modifier] - the BEM Modifier for the Block or Element; if undefined, selects the Block or Element unmodified.
*/
modifier =>

修复导入语句尾部的注释问题 (#1677)

这是我们需要特殊处理注释逻辑的又一场景!

// Before
import {
ExecutionResult,
DocumentNode,
/* tslint:disable */
SelectionSetNode,
} /* tslint:enable */ from 'graphql';

// After
import {
DocumentNode,
/* tslint:disable */
SelectionSetNode,
/* tslint:enable */
} from 'graphql';

处理成员链中的注释 (#1686, #1691)

此前我们已处理过部分注释位置问题,但随着新场景不断出现,现转为采用更通用的处理方案。希望未来不会再出现此类问题。

// Before
const configModel = this.baseConfigurationService.getCache().consolidated // global/default values (do NOT modify)
.merge(this.cachedWorkspaceConfig);

// After
const configModel = this.baseConfigurationService
.getCache()
.consolidated // global/default values (do NOT modify)
.merge(this.cachedWorkspaceConfig);

对嵌套箭头函数使用 expandLast 策略 (#1720)

// Before
f(action => next =>
next(action));

// After
f(action => next =>
next(action),
);

将 JSX 注释置于括号内 (#1712)

此改动主要影响 Facebook 工程师——当更新 Flow 版本时系统会自动添加 $FlowFixMe 注释。现在这些注释不再会被打乱。

// Before
const aDiv = /* $FlowFixMe */
(
<div className="foo">
Foo bar
</div>
);

// After
const aDiv = (
/* $FlowFixMe */
<div className="foo">
Foo bar
</div>
);

强制在多变量声明时换行 (#1723)

这是高频需求项。此前我们仅在行宽超过 80 列时才换行,现在只要存在至少一个带赋值的变量声明,无论行宽如何都会换行。

// Before
var numberValue1 = 1, numberValue2 = 2;

// After
var numberValue1 = 1,
numberValue2 = 2;

内联处理 | null 和 | void 类型 (#1734)

Flow 联合类型展开显示在包含多个对象时效果良好,但用于可空性检查时则显得怪异。现在我们对 | null| void 采用内联处理。

// Before
interface RelayProps {
articles:
| Array<
| {
__id: string,
}
| null
>
| null
}

// After
interface RelayProps {
articles: Array<{
__id: string,
} | null> | null,
}

在 implements 处换行而非 extends 处 (#1730)

不再在 extends 处强制换行,这将使带继承的类声明在需要换行时更自然。

// Before
class MyContractSelectionWidget
extends React.Component<
void,
MyContractSelectionWidgetPropsType,
void
> {
method() {}
}

// After
class MyContractSelectionWidget extends React.Component<
void,
MyContractSelectionWidgetPropsType,
void
> {
method() {}
}

内联处理单导入语句 (#1729)

与长 require 调用不换行的处理一致,现在当 import 语句仅导入单个内容时也不再换行。

// Before
import somethingSuperLongsomethingSuperLong
from "somethingSuperLongsomethingSuperLongsomethingSuperLong";

// After
import somethingSuperLongsomethingSuperLong from "somethingSuperLongsomethingSuperLongsomethingSuperLong";

为 SequenceExpression 添加换行能力 (#1749)

你知道吗?如果代码中不含语句,可以用 () 替代 {},用 , 替代 ;?现在你知道了。有人会在箭头函数返回值时利用此特性。虽然不推荐这么做,但 Prettier 实现支持很简单,那就加上吧 ¯\_(ツ)_/¯

// Before
const f = (argument1, argument2, argument3) =>
(doSomethingWithArgument(argument1), doSomethingWithArgument(
argument2
), argument1);

// After
const f = (argument1, argument2, argument3) => (
doSomethingWithArgument(argument1),
doSomethingWithArgument(argument2),
argument1
);

不再强制为空循环体换行 (#1815)

空循环体的 {} 不再被强制拆分为两行。

// Before
while (true) {
}

// After
while (true) {}

保留带注释的 switch case 间空行 (#1708)

// Before
switch (true) {
case true:
// Good luck getting here
case false:
}

// After
switch (true) {
case true:

// Good luck getting here
case false:
}

正确性保障

移除 ast-types 依赖 (#1743, #1744, #1745, #1746, #1747)

以往我们通过遍历 AST(使用 ast-types 的定义)来确定注释位置。当某些字段未声明时,这偶尔会导致问题——我们无法找到对应节点,导致注释被打印到错误位置或抛出异常。事实证明我们无需维护这种映射关系,只需遍历对象即可:只要节点拥有 type 字段,就将其视为有效节点。

// Before
Error: did not recognize object of type "ObjectTypeSpreadProperty"

// After
type X = {...Y/**/};
type X = {/**/...Y};

保留非常规 Unicode 空白符 (#1658, #1165) 由 @karl@yamafaktory 贡献

此前若在 JSX 文本中使用不可见字符,Prettier 会将其替换为常规空格。虽然不确定这种使用场景的实际意义,但现在我们会原样保留这些字符!

防止模板字面量尾部注释逃逸 (#1580, #1713, #1598, #1713) 由 @josephfrazier@k15a 贡献

我们曾用复杂且不稳定的逻辑处理模板字面量内的注释。针对 JSX {} 表达式,我们引入了优雅的解决方案:在 } 结束前设置边界——若存在未打印的注释,则一次性输出所有注释,添加换行符后再打印 }。现在我们将此逻辑应用于模板字面量处理 :)

// Before
`${0} // comment`;

// After
`${
0
// comment
}`;

正确添加 await 表达式括号 (#1513, #1595, #1593) 由 @bakkot@existentialism 贡献

由于我们采用枚举节点组合的方式(而非自动化方案)决定括号添加规则,仍存在少量特殊组合未被覆盖。本次更新通过精准添加必要括号并移除冗余括号,显著增强了 await 表达式的处理鲁棒性。

// Before
(await spellcheck) && spellcheck.setChecking(false);
new A((await x));

// After
await (spellcheck && spellcheck.setChecking(false));
new A(await x);

保留 Flow ObjectTypeProperty 中的 getter/setter 信息 (#1585) 由 @josephfrazier 贡献

又一项长尾问题的修复成果!

// Before
type T = { method: () => void };

// After
type T = { get method(): void }

为带泛型的单参数类型添加括号 (#1814)

另一类隐蔽的括号缺失问题被成功解决!

// Before
type ExtractType = <A>B<C> => D

// After
type ExtractType = <A>(B<C>) => D

Babylon 解析器支持非严格模式回退 (#1587, #1608) 由 @josephfrazier 贡献

为解析所有 JavaScript 代码,Babylon 解析器需判断文件是否采用严格模式。我们默认启用严格模式(覆盖多数场景),但当遇到类似 0775 的八进制字面量时会导致解析失败。现在遇到严格模式解析失败时,我们将自动切换至非严格模式重试。同时我们允许在 Node 文件中使用函数外部的 return 语句(符合 Node 规范)。

// Before
SyntaxError

// After
return 0775;

命令行工具

允许 --write--list-different 同时使用 (#1633)

当编写提交钩子时,通过单条命令同时告知用户实际变更内容非常有用。

CLI 执行时默认忽略 node_modules (#1683) by @thymikee

用户很容易误操作格式化 node_modules/ 目录(通常无需处理)。现已默认禁用此行为,若确实需要可通过 --with-node-modules 选项启用。

支持 glob 模式匹配点文件 (#1844) by @jhgg

我们在使用的 glob 解析库中启用了点文件遍历功能,这意味着 * 模式现在可匹配 .eslintrc 这类文件。