postcss-plugin-shared
English | 简体中文
postcss-plugin-shared is a shared utilities package used across the postcss-plugins monorepo to avoid duplicated logic (option merging, selector blacklists, declaration dedupe, exclude matching, unit regexes, numeric helpers, etc.).
Goals: small, stable, side-effect free, and reusable. This package intentionally does not include specific unit-conversion formulas (e.g. rem→px); it only provides generic building blocks.
Install / Usage
Inside this repo (pnpm workspace)
Use a workspace dependency:
// packages/your-plugin/package.json
{
"dependencies": {
"postcss-plugin-shared": "workspace:^"
}
}Then import utilities in your plugin:
import { createExcludeMatcher, remRegex } from 'postcss-plugin-shared'Outside this repo
If you publish this package to npm, install it normally:
pnpm add postcss-plugin-sharedThis package declares postcss as a peer dependency (^8).
Exports
Entry: packages/postcss-plugin-shared/src/index.ts
mergeOptions: option merging based ondefu(arrays use “override” strategy)createConfigGetter: create agetConfig(options?)helper based onmergeOptionstoFixed: stable rounding helper (avoids-0/precision noise)createUnitRegex: build a unit regex with configurable skip rulesremRegex/pxRegex: shared regexes for rem/px replacement (skips string literals,url(),var())blacklistedSelector: selector blacklist matcher (string includes / RegExp match)maybeBlacklistedSelector: likeblacklistedSelector, but returnsundefinedfor non-string selectorscreatePropListMatcher: builds a property matcher frompropList(supports*)createAdvancedPropListMatcher: advanced prop matcher (wildcards + negation) forstring[]createExcludeMatcher: builds an exclude matcher fromexclude(array or function)createSelectorBlacklistMatcher: selector blacklist matcher with optional cachedeclarationExists: checks whether a rule/decls already contains the sameprop/valueto avoid duplicateswalkAndReplaceValues: shared walker to replace declaration values and media params
API
mergeOptions(options, defaults)
Merges user options with defaults:
- Object fields follow
defusemantics (fallback todefaultswhen not provided inoptions) - Arrays are overridden: if both sides are arrays, the user array replaces the default array
import { mergeOptions } from 'postcss-plugin-shared'
interface Options {
propList: string[]
unitPrecision: number
}
const defaults: Options = { propList: ['*'], unitPrecision: 5 }
const resolved = mergeOptions<Options>({ propList: ['font-size'] }, defaults)
// resolved.propList === ['font-size']createConfigGetter(defaults)
Creates a strongly-typed getConfig(options?) function:
import { createConfigGetter } from 'postcss-plugin-shared'
const defaultOptions = { rootValue: 16, propList: ['*'] as string[] }
export const getConfig = createConfigGetter(defaultOptions)
getConfig() // => defaultOptions
getConfig({ rootValue: 10 }) // => merged resulttoFixed(number, precision)
Stable rounding helper:
- returns
0whennumber === 0 - preserves sign (supports negative values)
- uses
Number.EPSILONto reduce floating-point edge cases
import { toFixed } from 'postcss-plugin-shared'
toFixed(1.005, 2) // 1.01
toFixed(0, 5) // 0createUnitRegex(options)
Creates a global regex for unit replacement with configurable “skip” rules.
Notes:
- capture group 1 is the numeric portion
- defaults to skipping quoted strings,
url(...)andvar(...)
import { createUnitRegex } from 'postcss-plugin-shared'
const pxLike = createUnitRegex({ units: ['px', 'rpx'], ignoreCase: true })remRegex / pxRegex
Global regexes for String.prototype.replace, designed to reduce false positives:
- skip double-quoted strings
"..."and single-quoted strings'...' - skip
url(...) - skip
var(...) - capture group 1 is the numeric portion (e.g.
1.25)
import { remRegex } from 'postcss-plugin-shared'
const value = 'margin: 1rem 0; background: url("1rem.png")'
value.replace(remRegex, (m, num) => `${Number(num) * 16}px`)
// => margin: 16px 0; background: url("1rem.png")blacklistedSelector(blacklist, selector?)
Returns true if selector matches the blacklist:
blacklistsupportsstring | RegExp- returns
falsewhenselectoris not a string string:selector.includes(rule)RegExp:Boolean(selector.match(rule))
import { blacklistedSelector } from 'postcss-plugin-shared'
blacklistedSelector(['.ignore', /^\.no-/], '.ignore .a') // true
blacklistedSelector(['.ignore', /^\.no-/], '.no-test') // truemaybeBlacklistedSelector(blacklist, selector?)
Same matching logic as blacklistedSelector, but returns undefined when selector is not a string.
createPropListMatcher(propList)
Builds a matcher (prop: string) => boolean to decide whether a CSS property should be processed.
Rules:
- If
propListincludes'*', it matches everything. - String entries prefixed with
!exclude properties. Negated strings support!foo(exact) and glob patterns like!foo*,!*foo,!*foo*,!--wot-*-font-size. - Otherwise:
stringwithout*:prop.includes(rule)stringwith*: glob matchingRegExp:Boolean(prop.match(rule))
import { createPropListMatcher } from 'postcss-plugin-shared'
const match = createPropListMatcher(['font', /height$/])
match('font-size') // true (contains 'font')
match('line-height') // true (/height$/)
match('color') // false
const matchWithExcludes = createPropListMatcher(['*', '!font-size', '!padding*'])
matchWithExcludes('font-size') // false
matchWithExcludes('padding-right') // false
const matchCustomProps = createPropListMatcher(['*', '!--wot-*-font-size'])
matchCustomProps('--wot-body-font-size') // falsecreateAdvancedPropListMatcher(propList)
Advanced property matcher for string[] propList, compatible with postcss-pxtrans patterns:
*matches all propertiesfooexact match- strings containing
*use glob matching, such asfoo*,*foo,*foo*,!--wot-*-font-size !patternnegates (deny-list)
import { createAdvancedPropListMatcher } from 'postcss-plugin-shared'
const match = createAdvancedPropListMatcher(['*', '!border', 'font*', '*height'])
match('font-size') // true
match('border') // falsecreateExcludeMatcher(exclude)
Builds an exclude matcher (filepath?: string) => boolean.
excludecan beArray<string | RegExp>or(filePath) => boolean- returns
falsewhenfilepathisundefined
import { createExcludeMatcher } from 'postcss-plugin-shared'
const isExcluded = createExcludeMatcher([/node_modules/i, 'vendor'])
isExcluded('/a/node_modules/x.css') // true
isExcluded('/a/src/vendor.css') // truedeclarationExists(decls, prop, value)
Checks whether a rule already contains the same declaration (commonly used to avoid duplicates when replace: false and cloneAfter is used).
decls only needs a .some(...) method that iterates PostCSS ChildNodes (usually a Rule).
import { declarationExists } from 'postcss-plugin-shared'
// inside PostCSS visitor
if (!declarationExists(rule, decl.prop, nextValue)) {
decl.cloneAfter({ value: nextValue })
}Development
This package uses tsdown:
pnpm -C packages/postcss-plugin-shared devpnpm -C packages/postcss-plugin-shared build
Use cases
This package currently powers multiple plugins in this monorepo, e.g.:
postcss-rem-to-responsive-pixelpostcss-rem-to-viewportpostcss-pxtrans
If you want to extract more shared logic, this is the recommended place. Keep the scope:
- utilities only (no plugin state, no IO, no side effects)
- decoupled from conversion formulas (each plugin can define its own conversion)