A Prettier plugin that makes it less opinionated and a bit more expressive, by respecting your line breaks.
Penere lets you decide how code is laid out, by where you put the first line break, and formats everything else the way Prettier does.
Try it in the playground, side by side with stock Prettier.
// Written on one line, stays on one line (if it fits):
const sizes = [1, 2, 3]
// Broken after the opener, stays expanded:
const sizes = [
1, 2, 3
]Prettier already does this for object literals with
objectWrap: "preserve": if
there is a line break between { and the first property, the object stays
expanded. Penere applies the same rule everywhere else:
- Break after the opener (
[,(,{,<,${, before the first.of a chain, …) and the construct stays expanded. - Join it back onto one line and Prettier collapses it again, if it fits.
Everything you don't express this way is formatted by stock Prettier. Each rule
is an option, and wrap: "collapse" turns them all off, giving you exactly
stock Prettier.
Install Prettier and the plugin:
npm install --save-dev prettier prettier-plugin-penereAdd the plugin to your Prettier configuration:
// prettier.config.js
export default {
plugins: ['prettier-plugin-penere']
}That's all: Prettier now formats JavaScript, TypeScript, Flow and JSX with Penere, wherever it runs, in editors, on the command line, in pre-commit hooks, or through eslint-plugin-prettier. All options default to Penere's behavior, and can be set next to Prettier's own options:
// prettier.config.js
export default {
plugins: ['prettier-plugin-penere'],
semi: false,
singleQuote: true,
arrowParens: 'avoid',
trailingComma: 'none',
// Penere options:
argumentWrap: 'collapse',
clarifyMixedOperators: true
}On the command line, without a configuration file:
npx prettier --plugin prettier-plugin-penere --write .And through Prettier's API:
import * as prettier from 'prettier'
import penere from 'prettier-plugin-penere'
const formatted = await prettier.format(code, {
parser: 'babel',
plugins: [penere]
})wrap sets all *Wrap options at once, and each of them can be set on its own
to override it. They take "preserve" (Penere) or "collapse" (stock
Prettier), like Prettier's own objectWrap.
| Option | Default | Covers |
|---|---|---|
wrap |
"preserve" |
All *Wrap options that aren't set, and the ones below |
arrayWrap |
wrap |
Arrays, array destructuring and tuples |
parameterWrap |
wrap |
Function parameters |
argumentWrap |
wrap |
Call arguments |
conditionWrap |
wrap |
Conditions of if, while and do … while |
memberChainWrap |
wrap |
Member chains |
objectDestructuringWrap |
wrap |
Object destructuring |
ternaryWrap |
wrap |
Conditional expressions and types |
templateLiteralWrap |
wrap |
Template literal interpolations |
operatorWrap |
wrap |
Binary and logical operators, and assignments of them |
importExportWrap |
wrap |
Named imports and exports |
jsxAttributeWrap |
wrap |
JSX attributes |
typeParameterWrap |
wrap |
Type parameters and arguments |
extendsWrap |
wrap |
extends clauses of interfaces |
unionTypeWrap |
wrap |
Union types |
mappedTypeWrap |
wrap |
Keys of mapped types |
The other options are not about line breaks. When they aren't set, they follow
wrap too: Penere's value with "preserve", stock Prettier's with
"collapse".
| Option | "preserve" |
"collapse" |
Covers |
|---|---|---|---|
hugLastParameter |
true |
false |
Hug the last parameter if it's the only destructured one |
conciseStringArrays |
true |
false |
Fill arrays of strings like arrays of numbers |
breakComplexArrayItems |
false |
true |
Always break arrays of objects or arrays |
clarifyMixedOperators |
false |
true |
Add parentheses to mixed operators for readability |
preserveParentheses |
true |
false |
Keep redundant parentheses from the source |
Object literals are left to Prettier's own objectWrap, which already defaults
to "preserve".
The examples below show the input, and the output with each value of the option.
Break if the source breaks after [, and keep the rows of elements as written,
e.g. to group them. If the first row has only one element, every element goes
on its own line.
// Input
const sizes = [
1, 2, 3
]
const list = [
1,
2, 3
]
// arrayWrap: "preserve"
const sizes = [
1, 2, 3
]
const list = [
1,
2,
3
]
// arrayWrap: "collapse"
const sizes = [1, 2, 3]
const list = [1, 2, 3]Break all parameters if the source breaks after (.
// Input
function request(
url, method) {}
// parameterWrap: "preserve"
function request(
url,
method
) {}
// parameterWrap: "collapse"
function request(url, method) {}Break all arguments if the source breaks after (.
// Input
test(
foo(), bar())
// argumentWrap: "preserve"
test(
foo(),
bar()
)
// argumentWrap: "collapse"
test(foo(), bar())Break the condition of if, while and do … while statements if the source
breaks after (. Binary and logical conditions then break between their
operands, while nested groups only break if their source does. Like the { of
objects, only the line break after ( counts: to bring a condition back onto
one line, remove that one.
// Input
if (
data && !isEmptySchema(schema)) {
}
// conditionWrap: "preserve"
if (
data &&
!isEmptySchema(schema)
) {
}
// conditionWrap: "collapse"
if (data && !isEmptySchema(schema)) {
}Since Prettier 3.7, negated conditions are hugged by if (. With Penere, that's
up to you: written as if (!(, they stay hugged, and with a break after if (,
they stay broken.
// Input, and conditionWrap: "preserve"
if (
!(
wildcard ||
property && ['object', 'array'].includes(property.type)
)
) {
}
// conditionWrap: "collapse"
if (!(
wildcard ||
property && ['object', 'array'].includes(property.type)
)) {
}Expand the chain if the source breaks before a member, up to the first call.
// Input
object
.foo().bar()
expect(value)
.toBe(true)
// memberChainWrap: "preserve"
object
.foo()
.bar()
expect(value)
.toBe(true)
// memberChainWrap: "collapse"
object.foo().bar()
expect(value).toBe(true)Object destructuring follows objectWrap like object literals do, and
destructured parameters keep their own group, so that breaking the parameters
doesn't expand them.
// Input
const {
a, b } = object
// objectDestructuringWrap: "preserve"
const {
a,
b
} = object
// objectDestructuringWrap: "collapse"
const { a, b } = objectBreak the conditional if the source breaks before the consequent.
// Input
const value = condition
? a : b
// ternaryWrap: "preserve"
const value = condition
? a
: b
// ternaryWrap: "collapse"
const value = condition ? a : bInterpolations may break at their ${ and } boundaries instead of inside the
expression, and stay broken if the source breaks after ${.
// Input
const message = `${
pico.gray('<--')
} ${pico.bold(ctx.method)}`
// templateLiteralWrap: "preserve"
const message = `${
pico.gray('<--')
} ${pico.bold(ctx.method)}`
// templateLiteralWrap: "collapse"
const message = `${pico.gray('<--')} ${pico.bold(ctx.method)}`If the source breaks between any two operands, all operands on the same level break. Nested expressions that break are wrapped in parentheses, with the opening parenthesis on the line of the operator. Assignments, returns and arrow function bodies wrap broken expressions in parentheses as well. Literals, empty objects and arrays stay on the line of their operator.
// Input
function isEmpty(arg) {
return arg == null ||
isArrayLike(arg) && arg.length === 0 || isObject(arg) && Object.keys(arg).length === 0
}
const isValid = value => value != null &&
value !== ''
// operatorWrap: "preserve"
function isEmpty(arg) {
return (
arg == null ||
isArrayLike(arg) && arg.length === 0 ||
isObject(arg) && Object.keys(arg).length === 0
)
}
const isValid = value => (
value != null &&
value !== ''
)
// operatorWrap: "collapse"
function isEmpty(arg) {
return (
arg == null ||
isArrayLike(arg) && arg.length === 0 ||
isObject(arg) && Object.keys(arg).length === 0
)
}
const isValid = value => value != null && value !== ''Operands of mixed && and || follow the same rule: they're only wrapped in
parentheses when they break between their own operands. Operands that stay on
one line, like !ctor && !isModule(arg) below, or only break inside a nested
group, like ctor && (, don't get any. Parentheses you write are kept, see
preserveParentheses. Once an operand has parentheses of its own, the line break
after its ( decides whether it breaks, like the { of objects: to bring it
back onto one line, remove that one.
// operatorWrap: "preserve"
function isPlainObject(arg) {
const ctor = arg?.constructor
return (
!!arg && (
ctor && (
ctor === Object ||
ctor.name === 'Object'
) ||
!ctor && !isModule(arg)
)
)
}
const isSameShape = (
isPlainObject(before) && isPlainObject(after) || (
isArray(before) &&
isArray(after)
)
)Break named imports and exports if the source breaks after {.
// Input
import {
a, b } from 'x'
// importExportWrap: "preserve"
import {
a,
b
} from 'x'
// importExportWrap: "collapse"
import { a, b } from 'x'Break the attributes if the source breaks before the first one.
// Input
const el = <Foo
a="1" b="2" />
// jsxAttributeWrap: "preserve"
const el = (
<Foo
a="1"
b="2"
/>
)
// jsxAttributeWrap: "collapse"
const el = <Foo a="1" b="2" />Break type parameters and arguments if the source breaks after <.
// Input
type Pair<
A, B> = [A, B]
// typeParameterWrap: "preserve"
type Pair<
A,
B
> = [A, B]
// typeParameterWrap: "collapse"
type Pair<A, B> = [A, B]Keep a line break before the extends of interfaces, and put each extended
type on its own line if the source breaks between them.
// Input, and extendsWrap: "preserve"
interface InputSchema<$Item>
extends BaseSchema<$Item>,
SchemaTextMixin<$Item> {}
// extendsWrap: "collapse"
interface InputSchema<$Item>
extends BaseSchema<$Item>, SchemaTextMixin<$Item> {}Break unions that span multiple lines in the source, including a line break
before a leading |, which is also kept after the ? and : of conditional
types.
// Input
type Method =
| 'get' | 'post'
// unionTypeWrap: "preserve"
type Method =
| 'get'
| 'post'
// unionTypeWrap: "collapse"
type Method = 'get' | 'post'Only break after the [ of a mapped type key if the source does, and hug the
brackets otherwise.
// Input, and mappedTypeWrap: "preserve"
type Hooks = {
[key in `${'before' | 'after'}:${
| 'find'
| 'insert'
}`]?: Hook
}
// mappedTypeWrap: "collapse"
type Hooks = {
[
key in `${'before' | 'after'}:${
| 'find'
| 'insert'
}`
]?: Hook
}Hug the last parameter if it is an object or array pattern and no other parameter is, like Prettier hugs the last argument of a call.
// Input
async function request(api, { url, method, params, query, headers, data, timeout }) {}
// hugLastParameter: true
async function request(api, {
url,
method,
params,
query,
headers,
data,
timeout
}) {}
// hugLastParameter: false
async function request(
api,
{ url, method, params, query, headers, data, timeout }
) {}Fill arrays of strings as many per line as fit, like Prettier does for arrays of numbers.
// conciseStringArrays: true
const keys = [
'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten',
'eleven'
]
// conciseStringArrays: false
const keys = [
'one',
'two',
'three',
…
]Prettier always breaks arrays with more than one element if all of them are
objects or arrays with more than one item. Penere leaves that to arrayWrap.
// breakComplexArrayItems: false
const pairs = [{ a: 1, b: 2 }, { a: 3, b: 4 }]
// breakComplexArrayItems: true
const pairs = [
{ a: 1, b: 2 },
{ a: 3, b: 4 }
]Prettier adds parentheses to mixed && / ||, mixed * / /, % within
+ / -, and ?? within conditionals. Penere only keeps the ones you wrote.
Parentheses required by precedence or syntax, e.g. around ?? mixed with
||, are always kept.
// clarifyMixedOperators: false
const a = b && c || d
// clarifyMixedOperators: true
const a = (b && c) || dKeep parentheses around nested binary and logical expressions that you wrote, even if they are redundant.
// preserveParentheses: true
const a = (b * c) + d
// preserveParentheses: false
const a = b * c + d- Prettier
~3.9.0. Penere adjusts the documents Prettier builds, so it depends on Prettier's internals, and is released for each Prettier minor version. Penere has its own version number. - Languages: JavaScript, TypeScript, Flow and JSX, with the
babel,babel-flow,babel-ts,flow,typescript,acorn,espreeandmeriyahparsers. JavaScript embedded in other languages, like Vue or Markdown, goes through the same parsers, and is formatted by Penere too. JSON is left to stock Prettier. - Pug: expressions in templates are formatted by Penere with
@prettier/plugin-pug3.6.0 or newer. Older versions format them with stock Prettier. ternaryWrapdoesn't apply withexperimentalTernaries.
Prettier has no documented way for a plugin to change how a built-in language is printed. Penere re-exports Prettier's JavaScript parsers under its own AST format, so that Prettier uses Penere's printer, which wraps the built-in one.
For most rules, Penere lets Prettier print the node, and adjusts the result, e.g. by breaking a group or swapping a layout. If the result doesn't have the expected shape, it is left untouched, and you get stock Prettier's output. Expressions with operators are printed by Penere itself, based on Prettier's printer.
npm install
npm test
npm run playground # Run the playground locallytests/fixtures/<name>/input.*andoutput.*hold the expected Penere output, tested with thebabel,typescriptandflowparsers, or onlytypescriptfor.tsfiles.- The same fixtures are formatted with
wrap: "collapse", which must match stock Prettier exactly. - The examples in this README are tested too, with the style they're written in.
- The source is formatted with Penere itself:
npm run format. - The playground in
playground/is deployed to GitHub Pages on every push tomain.
Show the world you're using Penere →
[](https://github.com/ditojs/penere)MIT. Penere includes code from Prettier, © James Long and contributors.