Skip to content

pickier/pickier

Social Card of this repo

npm version GitHub Actions

Pickier

Fast linting and formatting. Minimal defaults. Extensible. Built for speed.

Features

  • Fast CLI with instant feedback
  • Lint and format in one tool via pickier run
  • Zero-config defaults; simple, typed pickier.config.ts when you need it
  • Import organization: splits type/value imports, sorts modules/specifiers, removes unused named imports
  • JSON and config sorting for common files (e.g. package.json, tsconfig.json)
  • Markdown linting with 53+ rules for documentation quality (headings, lists, links, code blocks, tables, etc.)
    • 27 rules support auto-fix for common formatting issues
  • Flexible formatting: indent, indentStyle (tabs or spaces), quotes, semi, trimTrailingWhitespace, maxConsecutiveBlankLines, finalNewline
  • Smart whitespace cleanup
  • ESLint-style plugin system for lint rules (load plugins, enable/disable rules, WIP labeling)
  • CI-friendly reporters (stylish, compact, JSON) and strict --max-warnings control
  • Programmatic API for custom tooling and editor integrations

Install

# as a dev dependency
bun add -D pickier

# or
npm i -D pickier

# or
pnpm add -D pickier

# or
yarn add -D pickier

You can also run it directly via npx without installing:

npx pickier --help

# or
bunx pickier --help

Quick Start

The unified pickier run command handles both linting and formatting:

# Auto-detect mode (lint + format)
pickier run .

# Lint everything
pickier run . --mode lint

# Auto-fix lint issues (safe fixes only)
pickier run . --mode lint --fix

# Preview fixes without writing
pickier run . --mode lint --fix --dry-run --verbose

# Format and write changes
pickier run . --mode format --write

# Check formatting without writing (CI-friendly)
pickier run . --mode format --check

CLI

pickier run [...globs]

The primary command. Routes to lint or format based on --mode.

Options:

Flag Description Default
--mode <mode> auto, lint, or format auto
--fix Apply safe fixes (lint mode) false
--dry-run Simulate fixes without writing (lint mode) false
--write Write changes to files (format mode) false
--check Check without writing, non-zero exit on differences (format mode) false
--max-warnings <n> Fail if warnings exceed n -1
--reporter <name> stylish, json, or compact stylish
--ext <exts> Comma-separated extensions (overrides config)
--ignore-path <file> Optional ignore file (e.g. .gitignore)
--config <path> Path to pickier config file
--cache Enable cache (lint mode, reserved) false
--verbose Verbose output false

Examples:

pickier run . --mode auto
pickier run src --mode lint --fix
pickier run "**/*.{ts,tsx,js}" --mode format --write
pickier run . --mode lint --reporter json --max-warnings 0

Note: The legacy pickier lint and pickier format commands are deprecated and will be removed in a future release. Use pickier run --mode lint and pickier run --mode format instead.

Configuration

Pickier works out-of-the-box. To customize, create pickier.config.ts in your project root. All fields are optional.

// pickier.config.ts
import type { PickierConfig } from 'pickier'

const config: PickierConfig = {
  verbose: false,
  ignores: ['**/node_modules/**', '**/dist/**', '**/build/**'],

  lint: {
    // which extensions to lint ('.ts' or 'ts' both supported)
    extensions: ['ts', 'js'],
    // stylish | json | compact
    reporter: 'stylish',
    // reserved (not used yet)
    cache: false,
    // -1 disables, otherwise fail when warnings > maxWarnings
    maxWarnings: -1,
  },

  format: {
    // which extensions to format
    extensions: ['ts', 'js', 'json', 'md', 'yaml', 'yml'],
    trimTrailingWhitespace: true,
    maxConsecutiveBlankLines: 1,
    // one | two | none
    finalNewline: 'one',
    // indentation width (code files)
    indent: 2,
    // 'spaces' | 'tabs'
    indentStyle: 'spaces',
    // preferred string quotes: 'single' | 'double'
    quotes: 'single',
    // when true, safely remove stylistic semicolons
    // (never touches for(;;) headers; removes duplicate/empty semicolons)
    semi: false,
  },

  rules: {
    // 'off' | 'warn' | 'error'
    noDebugger: 'error',
    noConsole: 'warn',
  },

  // Plugin rules for markdown, style, sorting, etc.
  pluginRules: {
    // Markdown linting (53+ rules available)
    'markdown/heading-increment': 'error',
    'markdown/no-trailing-spaces': 'error',
    'markdown/fenced-code-language': 'error',
    'markdown/no-duplicate-heading': 'warn',
  },
}

export default config

Plugin System

Pickier supports an ESLint-style plugin system for lint rules organized into focused categories.

Available Plugins:

Plugin Description Rules
pickier/ Sorting, import organization, and core checks 17+
general/ Error detection and possible problems 35+
quality/ Best practices and code quality 40+
style/ Code style enforcement (spacing, punctuation, line breaks, etc.) 50+
ts/ TypeScript-specific rules (type safety, formatting) 13+
regexp/ Regular expression safety 3+
markdown/ Markdown documentation linting 53+
lockfile/ Lock file validation 5+

Configure rules via pluginRules:

pluginRules: {
  'pluginName/ruleId': 'off' | 'warn' | 'error' | ['warn', options],
}

Custom Plugin Example:

// sample-plugin.ts
import type { PickierPlugin, RuleContext } from 'pickier'

export const samplePlugin: PickierPlugin = {
  name: 'sample',
  rules: {
    'no-todo': {
      meta: { docs: 'disallow TODO comments', recommended: true },
      check(content: string, ctx: RuleContext) {
        const issues = []
        const lines = content.split(/\r?\n/)
        for (let i = 0; i < lines.length; i++) {
          const col = lines[i].indexOf('TODO')
          if (col !== -1) {
            issues.push({
              filePath: ctx.filePath,
              line: i + 1,
              column: col + 1,
              ruleId: 'sample/no-todo',
              message: 'Unexpected TODO comment.',
              severity: 'warning',
            })
          }
        }
        return issues
      },
    },
    'experimental-check': {
      meta: { wip: true },
      check() {
        // not implemented yet — WIP rules surface errors with a :wip-error suffix
        throw new Error('WIP rule')
      },
    },
  },
}

Register the plugin in your config:

// pickier.config.ts
import type { PickierConfig } from 'pickier'
import { samplePlugin } from './sample-plugin'

const config: PickierConfig = {
  plugins: [samplePlugin],
  pluginRules: {
    'sample/no-todo': 'warn',
    'sample/experimental-check': 'error',
  },
}

export default config

Formatting Details

Semicolons

Controlled by format.semi (default false). When true, Pickier safely removes only stylistic semicolons:

  • Preserves for (init; test; update) headers
  • Removes duplicate trailing semicolons (e.g. foo();; -> foo();)
  • Removes lines that are just empty statements (;)
  • Keeps normal end-of-line semicolons otherwise (non-destructive)

Imports (TypeScript/JavaScript)

Groups and rewrites the top import block:

  • Splits type-only specifiers into import type { ... } from 'x'
  • Keeps default and namespace imports
  • Removes unused named specifiers (only when they have no alias)
  • Merges multiple imports from the same module

Sorting order:

  • By kind: type imports, side-effect imports, value imports
  • For modules: external before relative
  • For specifiers: A-Z by identifier

Spacing:

  • Ensures a single blank line between the import block and the rest of the file
  • Respects format.finalNewline at EOF

Built-in Rules

  • noDebugger — removes debugger statements when --fix is used
  • noConsole — controls severity (turn off for libraries that allow console logs)

Benchmarks

Measured on an Apple M3 Pro with Bun 1.3.9. Each tool uses equivalent settings (single quotes, no semicolons, 2-space indent). Pickier and Prettier use their in-memory APIs; oxfmt and Biome have no JS formatting API, so they are called via stdin pipe. Full benchmark source is in bechmarks/benchmarks/format-comparison.bench.ts.

In-memory / Programmatic API

Pickier formatCode() and Prettier format() run in-process. oxfmt and Biome are piped via stdin (no JS formatting API).

File Pickier Biome (stdin) oxfmt (stdin) Prettier
Small (52 lines, 1 KB) 41 us 40 ms 51 ms 1.59 ms
Medium (419 lines, 10 KB) 417 us 42 ms 50 ms 10.2 ms
Large (1,279 lines, 31 KB) 1.25 ms 46 ms 50 ms 28.1 ms

CLI (single file)

All four tools spawn a process and read the file from disk.

File Pickier Biome oxfmt Prettier
Small (52 lines) 37 ms 43 ms 68 ms 105 ms
Medium (419 lines) 38 ms 53 ms 71 ms 143 ms
Large (1,279 lines) 40 ms 90 ms 73 ms 187 ms

CLI Batch (all files, sequential)

Tool Time
Pickier 121 ms
Biome 187 ms
oxfmt 216 ms
Prettier 423 ms

Throughput (large file x 20)

Tool Time
Pickier 26 ms
Prettier 524 ms
Biome (stdin) 957 ms
oxfmt (stdin) 1,040 ms

Pickier's in-memory API is 22-39x faster than Prettier and orders of magnitude faster than tools that must spawn a process. On CLI, Pickier's compiled binary beats Biome's native Rust binary across all file sizes — 37ms vs 43ms on small files, and pulling further ahead on larger files. At throughput scale (20x large file), Pickier is 20x faster than Prettier and 37x faster than Biome/oxfmt.

# reproduce locally
bun bechmarks/benchmarks/format-comparison.bench.ts

Programmatic Usage

Call Pickier from code (Bun/Node) for custom tooling, editors, or pipelines.

import type { RunOptions } from 'pickier'
import { config, defaultConfig, run, runLint, runFormat, lintText } from 'pickier'

// Unified run (recommended)
const exitCode = await run(['.'], {
  mode: 'auto',
  fix: true,
  verbose: false,
})

// Lint specific directories
const lintCode = await runLint(['src', 'tests'], {
  fix: true,
  dryRun: false,
  reporter: 'json',
  maxWarnings: 0,
})

// Format specific globs
const fmtCode = await runFormat(['src/**/*.ts'], {
  write: true,
})

// Lint a single string
const result = await lintText('const x = 1;;', {
  filePath: 'virtual.ts',
})

// Access loaded config (from pickier.config.ts or defaults)
console.log('loaded config:', config)

Development

This repository contains Pickier's source under packages/pickier.

# install deps
bun i

# run tests (with coverage)
bun test --coverage

# build JS and type declarations
bun run -C packages/pickier build

# compile native binary for your platform
bun run -C packages/pickier compile

# compile all platform binaries
bun run -C packages/pickier compile:all

Try the CLI locally without publishing:

# run the TS entry directly
bun packages/pickier/bin/cli.ts --help

# run the built dist CLI
bun packages/pickier/dist/bin/cli.js run . --mode lint

# or the compiled native binary (after compile)
./packages/pickier/bin/pickier-<your-platform> --help

Testing

bun test

Changelog

Please see our releases page for more information on what has changed recently.

Contributing

Please review the Contributing Guide for details.

Community

For help, discussion about best practices, or any other conversation that would benefit from being searchable:

Discussions on GitHub

For casual chit-chat with others using this package:

Join the Stacks Discord Server

Postcardware

“Software that is free, but hopes for a postcard.” We love receiving postcards from around the world showing wherepickier is being used! We showcase them on our website too.

Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States 🌎

Sponsors

We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us.

Credits

License

The MIT License (MIT). Please see LICENSE for more information.

Made with 💙