@razorness/prettier-config
v0.2.1
Published
Shareable Prettier config plus a printer plugin for array bracket spacing, declaration bundling, vertical alignment and blank-line padding
Maintainers
Readme
@razorness/prettier-config
Shareable Prettier config plus the printer plugin it needs, in one package.
pnpm add -D @razorness/prettier-config prettier// package.json
{
"prettier": "@razorness/prettier-config"
}@ianvs/prettier-plugin-sort-imports, prettier-plugin-tailwindcss (when the project uses Tailwind)
and this package's own plugin come along, resolved by absolute path — nothing else to install.
What it does
Prettier's own options, set to semi, singleQuote, printWidth: 160, endOfLine: 'lf',
trailingComma: 'all', useTabs with tabWidth: 4, bracketSpacing,
htmlWhitespaceSensitivity: 'ignore' and importOrder: ['<THIRD_PARTY_MODULES>', '^[.]'].
On top of that, six rules Prettier has no option for — all six at once, which also shows how they interact:
// before
const ids = [0];
const retries = 3;
const config = {
name: 'widget',
enabled: true,
};
class Widget {
private cache = new Map();
render(id: string) {
const entry = this.cache.get(id);
const label = entry ?? 'unknown';
log(label);
emit(label);
return label;
}
}// after
const ids = [ 0 ],
retries = 3;
const config = {
name : 'widget',
enabled: true,
};
class Widget {
private cache = new Map();
render(id: string) {
const entry = this.cache.get(id),
label = entry ?? 'unknown';
log(label);
emit(label);
return label;
}
}[0]became[ 0 ].- The adjacent
consts became one declaration.configstayed out — its value spans lines. - Their
=line up, and so dorender's — note thatidsgot two spaces, not four: the padding is measured from the column each name starts in, andretriessits at an indent whileidssits behindconst. config's keys line up on the colon.- The class body gained a blank line at each end.
renderruns past four lines, so it gained blank lines around it and inside its braces.
bracketSpacing already covers {foo:true} → { foo: true }, .json included.
What the rules refuse to do
- Array spacing only when the array fits on one line; a broken array is left as Prettier laid it
out and
[]stays[]. Covers array literals, destructuring patterns and TS tuple types. - Bundling skips mixed kinds, a blank line between, an attached comment, a multi-line initialiser,
export/declare/using— andvar, unlessrazMergeDeclarations: "all". =alignment only for declarators that are a plain identifier with an initialiser. Type annotations and destructuring make the left-hand width depend on layout, so those are skipped — without disqualifying the declaration and without counting towards the column, the same way key alignment skips shorthand and spread members. It pads the names and never touches indentation: continuation declarators stay at the plain indent Prettier gives them, and the padding is measured from the column each name starts in —keyword.length + 1for the first,tabWidthfor the rest — so the separators still land in one column. Aligning the indentation to the keyword width instead is what an earlier version did, and it cannot be done cleanly underuseTabs: the keyword width is no multiple oftabWidth, so it needs spaces, and Prettier emits analignstring ahead of the tabs of any nestedindent. A declarator whose value spanned lines came out indented' \t'— spaces before a tab.- Key alignment only for objects already multi-line in the source — the same signal Prettier
itself uses, which is what keeps it stable across runs. Also covers
interfacebodies and type literals. Shorthand properties, methods and spreads are skipped without splitting a group; computed keys are skipped entirely, since a computed key can contain its own:. - Class body padding applies regardless of length. Empty bodies stay
{}.
Block padding
Two halves, two options, one shared razBlockPaddingMinLines threshold — but they weigh different
things, which is visible in the output:
| | measures | at the default of 5 |
| ------------------------------ | ------------------------------------ | -------------------------------- |
| razBlockPadding (outside) | the whole statement, braces included | a four-line body still qualifies |
| razBlockBodyPadding (inside) | only what sits between the braces | needs more than four lines |
// four lines between the braces: outside padding only
function four() {
one();
two();
three();
four();
}
// five lines: outside and inside
function five() {
one();
two();
three();
four();
five();
}
// too short for either half
function small() {
return 1;
}
if (x) tiny();Line counts decide it, never nesting depth. Here the describe body spans six lines and is padded;
the for body inside it spans four and stays tight:
describe('fixtures', () => {
for (const name of names) {
test(name, () => {
expect(format(name)).toBe(name);
});
}
});Both are measured on the formatted output, not the source, which is what makes the rule idempotent: a three-line function that formats to eight is padded on the first run, so format-on-save never rewrites the same file twice.
Options
| Option | Type | Default |
| ------------------------- | ------------------------------- | ------------- |
| razArrayBracketSpacing | boolean | true |
| razMergeDeclarations | "off" \| "const-let" \| "all" | "const-let" |
| razAlignDeclarators | boolean | true |
| razAlignObjectKeys | "off" \| "all" \| "groups" | "groups" |
| razClassBodyPadding | boolean | true |
| razBlockPadding | boolean | true |
| razBlockBodyPadding | boolean | true |
| razBlockPaddingMinLines | int | 5 |
razAlignObjectKeys: "groups" starts a fresh group at every blank line and after any property whose
value is forced onto several lines; "all" treats the object as one group.
// prettier.config.mjs
import config from '@razorness/prettier-config';
export default {
...config,
razBlockPaddingMinLines: 8,
razAlignObjectKeys : 'all',
};Tailwind
The default entry point loads prettier-plugin-tailwindcss only when the project can resolve
tailwindcss, so that projects without Tailwind keep their class attributes untouched — the plugin
ships its own copy of Tailwind v4 and would otherwise happily reorder them. Three things override the
detection: the @razorness/prettier-config/base entry point (never loads it), /tailwind (always
does), or RAZORNESS_PRETTIER_TAILWIND=0|1.
Detection resolves tailwindcss from process.cwd(), and it fails silently — class sorting just
stops happening. Prefer an explicit entry point wherever the cwd is not the project root.
Tailwind v4 derives class order from your stylesheet, so sorting needs tailwindStylesheet pointing at
the CSS file with @import "tailwindcss". Without it the plugin falls back to the bundled default
theme and treats your own utilities as unknown classes, sorting them to the front of the list.
Monorepos
Both failure modes bite at once here. Prettier runs from the workspace root, where tailwindcss is
not resolvable because it lives in the individual packages — so pick /tailwind explicitly. Relative
tailwindStylesheet paths resolve against the directory of the Prettier config that applies to the
file being formatted, which means one root config can point each package at its own stylesheet:
// prettier.config.mjs, at the workspace root
import config from '@razorness/prettier-config/tailwind';
export default {
...config,
overrides: [
{ files: 'packages/ui/**', options: { tailwindStylesheet: 'packages/ui/src/app.css' } },
{ files: 'apps/web/**', options: { tailwindStylesheet: 'apps/web/src/app.css' } },
],
};Per-package config files work too, but Prettier does not merge configs, so each one has to repeat the import.
Using the plugin on its own
// prettier.config.mjs
export default {
plugins : [ '@razorness/prettier-config/plugin' ],
razAlignObjectKeys: 'groups',
};How it works
The plugin claims only printers.estree and delegates every node to Prettier's built-in printer
before patching the doc it returns. Wrapping parsers instead — the usual approach — would collide
with prettier-plugin-tailwindcss, which claims the JS/TS parsers and only special-cases a hardcoded
list of plugins: depending on plugin order, either Tailwind's class sorting or these rules would
silently stop working.
That this is possible at all is narrower than it looks. From Prettier's
src/main/normalize-format-options.js:
const printerPlugin = parserPlugin.printers?.[parser.astFormat]
? parserPlugin // the parser's own plugin wins outright
: getPrinterPluginByAstFormat(rawOptions.plugins, ...); // only then is findLast consultedIf the plugin owning the parser also ships a printer for that astFormat, findLast is never
reached. Prettier's babel, typescript, acorn and flow plugins ship parsers only — the estree
printer sits in a separate plugin — so the lookup falls through and a later printers.estree wins. Its
html, postcss, markdown and yaml plugins bundle both, making those printers unreachable this
way. JS/TS is the only family where a printer-only plugin is possible, which is also why SFC block
sorting is out of reach.
The risk condition, then: a plugin owning parsers.babel that added its own printers.estree would
shadow this one. Tailwind currently registers only printers['svelte-ast'].
test/integration.test.ts guards the combination; test/noop.test.ts asserts that with every rule
disabled the output is byte-identical to vanilla Prettier across three parsers.
Known limitations
package.jsonis the one file the rules do not reach. Prettier formats it with thejson-stringifyparser, the only JSON parser whoseastFormatisestree-jsonrather thanestree, and this plugin claimsestree. So its arrays stay one entry per line and its keys stay unaligned. Every other.jsonand.jsoncfile — tsconfigs, lint configs, i18n message catalogues — does get key alignment and[ … ]spacing, which is worth knowing before you point this config at a repo full of them.- Class bodies are not colon-aligned — a class mixes
foo: string = 'a'withfoobar = 1, so there is no single separator to align on. They get blank-line padding instead. enummembers are not aligned; they use=, not:.- Block padding does not apply between statements inside a
switchcase, only around theswitch.
Development
TypeScript 6, no bundler: tsc emits JavaScript, declarations and maps in one pass, and dist/
mirrors src/ file for file. Vitest runs against src/, so coverage measures the real sources — and
its thresholds are pinned at 100% for lines, branches, functions and statements, with no
v8 ignore escape hatches. .prettierrc.mjs re-exports the built config, so the repo is formatted by
the plugin it ships and pnpm format:check needs a build first.
node scripts/dump-doc.mjs 'const a = [1];' ArrayExpression # inspect Prettier's Doc IR
node scripts/try.mjs 'const a = [1];' --twice # format a snippet, check stability
node scripts/generate-expected.mjs # regenerate test fixturesEvery doc patch is matched against structures read off the real printer with dump-doc, and returns
the doc untouched when the shape is not what it expects. After changing a rule, regenerate the
fixtures and read the diff — the generator will happily record a regression as the new expectation.
License
MIT. Includes code adapted from @huggingface/prettier-plugin-vertical-align (ISC) and from Prettier's
internals (MIT) — see LICENSE.
