Skip to content

About

Prettier that respects your line breaks: expressive, controllable formatting on top of stock Prettier

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Penere

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
]

The idea

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.

Usage

Install Prettier and the plugin:

npm install --save-dev prettier prettier-plugin-penere

Add 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]
})

Options

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.

arrayWrap

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]

parameterWrap

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) {}

argumentWrap

Break all arguments if the source breaks after (.

// Input
test(
  foo(), bar())

// argumentWrap: "preserve"
test(
  foo(),
  bar()
)

// argumentWrap: "collapse"
test(foo(), bar())

conditionWrap

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)
)) {
}

memberChainWrap

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)

objectDestructuringWrap

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 } = object

ternaryWrap

Break 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 : b

templateLiteralWrap

Interpolations 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)}`

operatorWrap

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)
  )
)

importExportWrap

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'

jsxAttributeWrap

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" />

typeParameterWrap

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]

extendsWrap

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> {}

unionTypeWrap

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'

mappedTypeWrap

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
}

hugLastParameter

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 }
) {}

conciseStringArrays

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',
  …
]

breakComplexArrayItems

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 }
]

clarifyMixedOperators

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) || d

preserveParentheses

Keep 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

Compatibility

  • 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, espree and meriyah parsers. 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-pug 3.6.0 or newer. Older versions format them with stock Prettier.
  • ternaryWrap doesn't apply with experimentalTernaries.

How it works

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.

Development

npm install
npm test
npm run playground       # Run the playground locally
  • tests/fixtures/<name>/input.* and output.* hold the expected Penere output, tested with the babel, typescript and flow parsers, or only typescript for .ts files.
  • 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 to main.

Badge

Show the world you're using Penere → code style: penere

[![code style: penere](https://img.shields.io/badge/code_style-penere-ffaa00.svg?style=flat-square)](https://github.com/ditojs/penere)

License

MIT. Penere includes code from Prettier, © James Long and contributors.

About

Prettier that respects your line breaks: expressive, controllable formatting on top of stock Prettier

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages