Skip to content

Preset Authoring Guide

How to design, organize, test, and publish custom presets for postcss-rule-unit-converter.

Decide The Right Shape

Use a single preset when:

  • one source unit maps to one target unit
  • one option object controls one conversion rule
  • ordering against other rules is simple

Use a preset group when:

  • one app mode needs multiple related rules
  • multiple units should normalize into one target unit
  • you want to ship one reusable opinionated bundle

Authoring A Single Preset

ts
import type { PresetFactory } from 'postcss-rule-unit-converter'
import { definePreset } from 'postcss-rule-unit-converter'

interface RemToDpOptions {
  rootValue?: number | ((input: import('postcss').Input) => number)
  to?: string
  minValue?: number
}

export const remToDp: PresetFactory<RemToDpOptions> = definePreset((options = {}) => {
  const { rootValue = 16, to = 'dp', minValue } = options

  return {
    from: 'rem',
    to,
    minValue,
    transform(value, context) {
      const resolvedRootValue = typeof rootValue === 'function'
        ? rootValue(context.input)
        : rootValue
      return value * resolvedRootValue
    },
  }
})

Guidelines:

  • keep option names domain-specific
  • return one ConversionRule
  • prefer factor when the rule is static
  • use transform only when you need input-aware or context-aware logic

Authoring A Preset Group

ts
import type { PresetGroupFactory } from 'postcss-rule-unit-converter'
import { definePresetGroup } from 'postcss-rule-unit-converter'

interface MobilePresetOptions {
  rootValue?: number
  ratio?: number
  viewportWidth?: number
}

export const mobilePresetGroup: PresetGroupFactory<MobilePresetOptions> = definePresetGroup((options = {}) => {
  const {
    rootValue = 16,
    ratio = 2,
    viewportWidth = 375,
  } = options

  return [
    {
      from: 'rem',
      to: 'rpx',
      factor: rootValue * ratio,
    },
    {
      from: 'px',
      to: 'rpx',
      factor: ratio,
    },
    {
      from: 'vw',
      to: 'rpx',
      factor: viewportWidth * ratio / 100,
    },
  ]
})

Guidelines:

  • group rules that belong to one target system
  • keep order explicit inside the returned array
  • avoid mixing unrelated concerns into one group

Option Design

Prefer options that reflect conversion intent:

  • rootValue
  • viewportWidth
  • viewportHeight
  • ratio
  • to
  • minValue

Avoid leaking implementation details into public options unless users truly need them.

Context-Aware Rules

Use RuleContext when behavior depends on:

  • current file path
  • current property name
  • current selector
  • original unit casing
  • exact matched source text

Example:

ts
const rule = {
  transform(value, context) {
    if (context.prop === 'letter-spacing') {
      return value * 2
    }

    if (context.rawUnit === 'PX') {
      return {
        value,
        unit: 'ch',
      }
    }

    return value
  },
}

File Organization

Recommended package layout:

text
src/
  presets/
    rem.ts
    viewport.ts
    mobile.ts
  index.ts
  presets.ts
test/
  rem.test.ts
  mobile.test.ts

Recommended export shape:

ts
export { mobilePresetGroup } from './presets/mobile'
export { remToDp } from './presets/rem'

Keep preset definitions in focused modules instead of one large file once the preset count grows.

Testing Strategy

Test presets at the plugin boundary, not only as raw objects.

ts
import postcss from 'postcss'
import unitConverter from 'postcss-rule-unit-converter'
import { remToDp } from '../src/presets/rem'

it('converts rem to dp', () => {
  const result = postcss([
    unitConverter({
      rules: [remToDp({ rootValue: 20 })],
    }),
  ]).process('.rule{font-size:1rem}', { from: undefined }).css

  expect(result).toBe('.rule{font-size:20dp}')
})

Recommended coverage:

  • default options
  • custom options
  • rule ordering
  • propList / selector blacklist interaction
  • dynamic input-based options
  • raw match context branches

Composition Strategy

Use composeRules(...) when app code mixes:

  • your local preset groups
  • built-in presets
  • one-off rules

Example:

ts
unitConverter({
  rules: composeRules(
    mobilePresetGroup({ ratio: 2 }),
    {
      from: 'em',
      to: 'rpx',
      factor: 32,
    },
  ),
})

Publishing Strategy

If presets are app-specific:

  • keep them in your app config package
  • export them from one local module

If presets are reused across multiple projects:

  • publish a small wrapper package
  • export typed preset factories
  • keep tests close to presets

Common Mistakes

  • relying on implicit rule order
  • mixing unrelated conversions in one preset group
  • exposing too many low-level options too early
  • testing the raw preset object but not the real plugin output
  • forgetting that first match wins

Rule Of Thumb

  • use one preset for one conversion idea
  • use one preset group for one target unit system
  • use app code to compose groups, not to re-implement conversions repeatedly

Built for CSS that moves between screens.