postcss/postcss
github.com/postcss/postcss ↗ · 114 files · 630 KB
PostCSS PostCSS is a tool for transforming styles with JS plugins.
Overview
PostCSS 8.5.28 is a JavaScript library for developers building CSS transformations, linters, and build-tool integrations. It parses CSS into a mutable, source-aware AST, runs caller-supplied plugins, and emits CSS with optional chained source maps. Its distinctive mechanisms include mutation-aware visitor rescanning, preservation of original formatting, replaceable parsers and stringifiers, and a separate no-plugin processing shortcut. This brief reflects read-only inspection at commit 7ac902664902289ea0cb9d6b1096b81e8d33bbf5.
System map
The package is an embeddable CSS-processing library distributed through CommonJS and ESM entrypoints with TypeScript declarations. Its runtime separates input handling, tokenization, parsing, plugin orchestration, AST mutation, and serialization. Processing state lives in memory; source-map discovery introduces a filesystem-read boundary, while runners own output persistence. Package scripts and GitHub Actions define validation and publishing rather than an application server deployment.
- package.json routes require to lib/postcss.js, import to lib/postcss.mjs, and typings to lib/postcss.d.ts; the ESM module re-exports the CommonJS API.
- postcss(...plugins) accepts either variadic plugins or one array and constructs Processor; the entrypoint also exports parse, stringify, fromJSON, node factories, and AST classes.
- Processor owns the normalized plugin list and selects NoWorkResult only when plugins, parser, stringifier, and syntax are all absent; other processing uses LazyResult.
- The default parser boundary is parse → Input → Parser → tokenizer; Parser constructs a Root and consumes tokens through nextToken rather than materializing a complete token list.
- Node supplies common metadata and operations, Container supplies children and traversal, Root represents CSS, and Document contains roots; Result holds processor, root, options, messages, CSS, and map.
- The three runtime dependencies are nanoid, picocolors, and source-map-js; browser mappings disable filesystem/path/URL support, terminal highlighting, and source-map-js, with availability checks in Input and MapGenerator.
- Existing validation scripts cover ESLint, declaration checks, version consistency, c8 coverage, parser integration, and bundle size; unit tests run through uvu with ts-node transpile-only.
- Tag pushes trigger cleaned-package npm publishing and changelog-based GitHub releases in separate jobs; tests, docs, and development configs are excluded through .npmignore, and publishing is not gated by a test job inside release.yml.
Feature catalog
The user-facing surface is a JavaScript API for transforming and inspecting CSS. Callers can use plugin processing, direct parsing, programmatic AST construction, custom syntaxes, diagnostics, source maps, and JSON hydration. Ecosystem tools supply features such as prefixing and linting, while external runners supply CLI and build-tool integration. Core features preserve source and formatting information to support those integrations.
- Callers can configure a reusable Processor with postcss(plugins), append plugins through use(plugin), and process multiple CSS inputs with per-call options.
- LazyResult supports synchronous css/root/messages access and promise-style then, catch, and finally; synchronous processing throws when a plugin returns a promise.
- The public API exposes factories for comments, at-rules, declarations, rules, roots, and documents, plus cloning, replacement, sibling insertion, and removal operations.
- Container offers mutation-aware walk plus type-specific walkAtRules, walkComments, walkDecls, and walkRules; applicable filters accept exact strings or regular expressions, and callbacks can stop traversal with false.
- ProcessOptions accepts parser, stringifier, or combined syntax replacements, along with document, from, to, map, and unsafeMap; custom parsers can return Root or Document.
- Source-map options include inline/external output, annotation strings or callbacks, source-name overrides, absolute paths, previous maps, and optional source content.
- Node.error creates source-positioned CssSyntaxError objects, Node.warn attaches nodes to result warnings, and Result.warnings filters the shared message channel by type.
- Node.toJSON and postcss.fromJSON provide AST serialization/hydration; list.comma, list.space, and list.split separate values while respecting quotes, escapes, comments, and parentheses.
Data flow
CSS enters as text or a string-convertible value and gains an Input object that owns original text, source identity, and previous-map information. The parser turns token offsets into AST source positions and retains formatting separately in raws. Plugins mutate the shared tree and append messages to Result, after which a builder emits CSS and optionally source mappings. JSON serialization provides a separate transport boundary; neither that API nor normal output generation writes the resulting artifact to disk.
- Input rejects null, undefined, and objects without toString, converts accepted input to text, strips an initial BOM, and keeps opts.document separately from the parsed CSS.
- Input resolves relative from paths, preserves absolute paths and URL-like identifiers, loads PreviousMap when supported, and assigns a non-secure nanoid-based diagnostic ID when no file identity exists.
- Tokenizer emits token arrays containing text and offsets; Parser.init attaches a shared Input and start position, while declaration parsing records an exclusive end offset.
- Parser.raw separates cleaned selector/value/parameter text from its original token spelling; Stringifier.rawValue reuses that spelling only while the semantic value still matches.
- LazyResult accepts existing Root/Document trees or previous Result/LazyResult objects without reparsing; it clears cleanliness markers and carries a previous result's map into opts.map.prev.
- Visitor callbacks receive helpers containing the public API and current Result; proxies mark selected property changes dirty, and markDirty propagates invalidation through ancestors.
- Serialization either concatenates builder chunks directly or lets MapGenerator track generated positions, attach source mappings, compose previous maps, and return CSS with an optional external map; the documented caller writes files.
- Node.toJSON omits parent/proxy links and deduplicates Inputs using inputId; fromJSON restores Input/PreviousMap prototypes, resolves input references, constructs nodes bottom-up, and reconnects child.parent without spacing normalization.
Behavior traces
The investigation followed initialization, plugin transformation, no-plugin processing, source-map loading/output, and JSON round-tripping through their implementation files. Plugin setup and parsing occur before deferred transformation, so LazyResult is only partly lazy. Sync and async traversal share event semantics but use separate execution loops. Branch conditions matter: no-plugin output, Document hooks, custom stringifiers, and inline maps have different behavior from the ordinary Root transformation path.
- Initialization: the ESM entry imports postcss.js → postcss.js loads classes/functions and calls LazyResult.registerPostcss → postcss(plugins) constructs Processor → Processor.normalize resolves creators, wrappers, plugin packs, objects, or legacy functions.
- Parse flow: postcss.parse(css, opts) → new Input → new Parser → createTokenizer → Parser.parse dispatches token kinds → other distinguishes rules/declarations → init attaches parent/source → endFile closes parsing or reports an unclosed block.
- Async transformation: Processor.process → LazyResult constructor parses and runs prepare(result) → then/async → runAsync → runOnRoot for each plugin, awaiting promises → prepareVisitors → visitTick passes until clean → OnceExit → stringify.
- Sync transformation: reading css → stringify → sync → runOnRoot → prepareVisitors → walkSync until clean → OnceExit → CSS generation; promise-returning root hooks or visitors trigger getAsyncError.
- Insertion during traversal: a visitor calls rule.after(child) → Container.insertAfter normalizes/reparents the child and adjusts live iterator slots → visitTick/walkSync advance only after the current subtree finishes; regression tests specify inserted siblings appearing before exit events.
- No-plugin flow: Processor.process selects NoWorkResult → constructor keeps the CSS string and generates or clears map annotations → async/sync returns Result → accessing root invokes parse once and caches either the AST or its error.
- Source-map flow: Input → PreviousMap.loadAnnotation → loadMap chooses explicit prev, inline data, or an annotation file → guarded loadFile/consumer → MapGenerator.generateString → setSourcesContent/applyPrevMaps/addAnnotation → inline CSS or external map returned to the caller.
- JSON round-trip: root.toJSON builds node objects and an input table → caller serializes/transports them → fromJSON hydrates Inputs and constructs descendants before parents → root.toResult creates an empty Processor/LazyResult and serializes; tests cover both V8 transport and JSON formatting preservation.
Testing signals
Tests and CI were inspected, not executed; no passing result or achieved coverage is asserted. The bounded inspection found uvu unit tests, declaration checks, external parser fixtures, integration testing, and a separate fuzz target. Tests specify important contracts for async ordering, insertion during traversal, deep trees, source-map trust boundaries, and serialization. Coverage configuration excludes a critical map module, and the inspected fuzz harness has limits that prevent treating it as comprehensive validation.
- The inspected unit suites use uvu and uvu/assert; package scripts select test files ending in .test.ts or .test.js, transpile TypeScript without type checking, and run declaration validation separately through check-dts.
- Parser tests compare ASTs against postcss-parser-tests fixtures and assert syntax-error positions; test/integration.js processes real CSS through parse().toResult with map annotations disabled.
- Processor and visitor tests specify sequential async plugin completion, custom parser/stringifier support, dynamically prepared listeners, and parity of inserted-sibling ordering between sync and async walks.
- A 6,000-level parser regression exercises stringify, clone, toJSON/fromJSON, and walk; a separate visitor regression checks synchronous mutation and the second pass over a changed declaration.
- Source-map tests cover rejection of non-.map files, paths outside the CSS directory, and missing from; they also specify unsafeMap opt-out, default/disabled source content, and annotation cleanup in the no-work branch.
- Security-related regressions specify __proto__ rejection during hydration and HTML delimiter escaping across chunks, while explicitly testing custom-stringifier and Document-raw exceptions.
- c8 is configured to require 100% lines for its included scope but excludes lib/map-generator.js; the Jazzer target exercises parse/stringify/JSON/list helpers, accepts CssSyntaxError on reparsing, and its processor branch uses an empty plugin list.
- The inspected CI definition runs the full package test script on Node 26, unit tests on Node 24/22, adapted old-runtime tests on Node 20 through 10, and unit tests on Windows; these are configured jobs, not observed successful runs.
Risk map
These are source observations and explicitly labeled hypotheses, not confirmed vulnerabilities. Sensitive boundaries include annotation-driven filesystem reads, caller-provided executable plugins, HTML embedding, and serialized AST hydration. The implementation contains concrete protections, but their scope and bypass options matter. Testing and documentation also have limits that future work should preserve or examine without assuming whole-repository coverage.
- PreviousMap performs synchronous filesystem reads; annotation-derived paths require a .map suffix, from, and realpath-based containment under the CSS directory, while unsafeMap and map.prev callbacks deliberately bypass these restrictions.
- Hypothesis: realpath containment checks and the subsequent existsSync/readFileSync operate separately on pathname state, so an attacker able to change relevant filesystem entries between those operations could warrant race-condition investigation; no exploit was reproduced. (inferred)
- Plugin creators, prepare hooks, root callbacks, and visitors execute directly in the caller's process with the live Result and API helpers; the inspected execution path provides no isolation boundary around those callbacks.
- HTML delimiter escaping is scoped to Roots serialized by the default SafeStringifier; custom Stringifier use, individual-node output, and Document markup raws have different guarantees, and escaping modified chunks reduces source-map precision.
- NoWorkResult returns its retained CSS without routing it through SafeStringifier and delays parsing until root access; reading css or awaiting an untouched result therefore does not establish parser validation or default Root escaping.
- Visitor processing loops until isClean; the plugin guide explicitly warns that unconditional insertion can create an infinite loop, so convergence depends on plugin guards or processed markers.
- Document is a public AST type and inherits Node.toJSON, but fromJSON's type dispatch only accepts root, decl, rule, comment, and atrule; Document hydration reaches the unknown-type error branch.
- Validation limits include map-generator.js being excluded from coverage enforcement and fuzz reparsing accepting CssSyntaxError; the fuzz processor branch cannot test plugin visitor execution because it constructs postcss() without plugins.
Extension points
The primary extension boundary is the plugin interface, with creators, packs, per-result preparation, filtered visitors, and lifecycle hooks. Parser and stringifier options provide a separate syntax boundary. AST factories and mutation methods allow extensions to construct or move nodes while retaining source information. Messages connect plugins to runners, while internal registration functions wire modules together and should be distinguished from public plugin APIs.
- Processor.normalize accepts creators marked postcss=true, postcss wrappers, objects containing plugins arrays, named plugin objects, and legacy functions; Processor.use exposes incremental registration.
- Plugin.prepare(result) can create per-processing state and return runtime listeners that are merged over the original plugin; Helpers supplies result, postcss, and the public API.
- PLUGIN_PROPS defines recognized lifecycle events; prepareVisitors builds listener arrays in plugin order, expands '*' handlers, lowercases named filters, and rejects unknown capitalized event names.
- Declaration and AtRule visitor filters use lowercased property/name keys; getEvents schedules general enter, filtered enter, children where applicable, general exit, and filtered exit.
- Custom parsing resolves syntax.parse first and then an explicit parser override; custom serialization resolves syntax.stringify first and then an explicit stringifier override, accepting function or method-bearing forms.
- Stringifier subclasses can override traversal methods; body only uses its built-in stack expansion when atrule, block, body, rule, and stringify retain their base implementations, preserving subclass dispatch otherwise.
- Plugins can append dependency and dir-dependency messages, including optional directory globs; runner guidelines assign watching and rebuild behavior to consumers of result.messages.
- Internal registerParse/registerRule/registerAtRule/registerRoot and Root/Document processor registrations resolve module dependencies; Parser.precheckMissedSemicolon is separately marked as a Safe Parser hook.
Contributor handoff
Begin with the package contract and trace the affected runtime branch before proposing edits. Preserve source positions, formatting raws, parent ownership, dirty propagation, and mutation-aware iterator behavior together. Tests and CI were inspected, not executed; their existence is not a passing result, and this inventory is bounded rather than exhaustive coverage. Continued work under the current authority must remain read-only, with executable validation deferred to a separately authorized task.
- Read package.json, docs/architecture.md, postcss.js, Processor, and LazyResult first for processing changes; the package exposes both ESM/CommonJS surfaces and a no-work branch that must be considered separately.
- AST edits should preserve iterator timing across Container.insertAfter and both LazyResult walkers; existing regressions compare inserted-sibling order across sync and async processing.
- Preserve raws and source when creating or hydrating nodes: cloneNode shares source references, plugin guidance recommends cloning for maps, and fromJSON attaches children after construction to avoid overwriting their spacing.
- Read PreviousMap and its boundary tests before changing map loading; distinguish annotation paths from trusted map.prev callbacks and the explicit unsafeMap option rather than applying one rule to all inputs.
- For authorized validation later, select the relevant uvu suite first, then existing test:types, test:integration, test:lint, or test:coverage scripts as needed; tests and CI in this brief were only inspected, with map-generator.js outside coverage enforcement.
- Preserve the declared Node 10/12/14+ compatibility and browser-disabled modules; old-runtime tests adapt node: imports and ts-node settings, while .editorconfig requires two spaces, LF, and a final newline.
- Runtime API changes require reviewing adjacent declarations and both export surfaces; contributing guidance uses classes for runtime classes, namespaces for related functions, and interfaces for custom types, while test/version.js requires package and Processor versions to agree.
- Treat architecture/security prose as guidance requiring source verification: architecture token examples describe line/column tuples while the tokenizer emits offsets, and THREAT_MODEL describes a minimumReleaseAge cooldown not configured in the inspected pnpm-workspace.yaml.
Languages
| Language | Files | Share of source |
|---|---|---|
| TypeScript | 49 | 58% |
| JavaScript | 36 | 42% |
| Markdown | 16 | docs/config |
| YAML | 4 | docs/config |
| JSON | 3 | docs/config |
Start reading here
Probable source entrypoint: lib/ — inferred from the directory tree and manifest, not the upstream docs.
Source roots
lib/ (52 files)
Source layout — lib/
| Path | Files |
|---|---|
lib/at-rule.d.ts | 1 |
lib/at-rule.js | 1 |
lib/comment.d.ts | 1 |
lib/comment.js | 1 |
lib/container.d.ts | 1 |
lib/container.js | 1 |
lib/css-syntax-error.d.ts | 1 |
lib/css-syntax-error.js | 1 |
lib/declaration.d.ts | 1 |
lib/declaration.js | 1 |
lib/document.d.ts | 1 |
lib/document.js | 1 |
lib/fromJSON.d.ts | 1 |
lib/fromJSON.js | 1 |
Repository structure
| Top-level path | Files |
|---|---|
lib/ | 52 |
test/ | 33 |
docs/ | 13 |
.github/ | 4 |
Docs found: README.mddocs/CODE_OF_CONDUCT.mddocs/INCIDENT_RESPONSE.mddocs/README-cn.mddocs/README.mddocs/SECURITY.mddocs/THREAT_MODEL.mddocs/architecture.mddocs/guidelines/plugin.mddocs/guidelines/runner.md
Package
package.json — postcss v8.5.28, 21 declared dependencies.
@logux/eslint-config@logux/oxc-configs@size-limit/preset-small-lib@types/nodec8check-dtsconcat-with-sourcemapseslintmultiocularnanodelaynanoidnanospy
CI & config entrypoints
README.md— Repository readme.github/workflows/release.yml— GitHub Actions workflow.github/workflows/test.yml— GitHub Actions workflowpackage.json— npm scripts: old, test, test:coverage, test:integration, test:lint, test:size, test:types, test:version
Structural health signals
Average score 8.6/10 across 59 analyzed source files; 6 hotspots surfaced.
lib/parser.js— 626 lines, score 6 (Large file, Deep nesting)lib/stringifier.js— 457 lines, score 6 (Large file, Deep nesting)lib/lazy-result.js— 626 lines, score 6.2 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
7ac9026649022026-09-29 — Do not split list.comma() and list.space() on separators inside comments (#2162)ebffa97faa302026-09-24 — Add postcss-smooth-corners plugin57d6401ccca62026-09-18 — Speed up source map cleaning7e46a036aa552026-09-18 — Speed up parser7f5c417f1d2d2026-09-18 — Improve </style> protection for the case when beffore nodes have different parts of attacker code