Which ordering guarantees does PostCSS provide when plugins rewrite the same declaration?
0 reputation · 20 Sept 2023, 07:02 UTC
0 reputation · 20 Sept 2023, 07:02 UTC
PostCSS walks a single AST sequentially through plugins registered with use(). It does not topologically sort plugins or detect conflicts, so when two plugins rewrite the same declaration, the final AST depends only on execution order and on whether each plugin mutates nodes in place or replaces them.
This leaves plugin sequencing as a manual decision. In a shared processor configuration, inserting a new plugin earlier in the chain can silently change output for rules that multiple plugins target. Because PostCSS preserves original whitespace in raws and only synthesizes missing pieces, semantically equivalent CSS can also serialize differently depending on node replacement order.
Which ordering guarantees does PostCSS provide when plugins rewrite the same declaration? If a plugin declares dependencies, does the processor validate them before walking the AST? What is the recommended way to keep order-sensitive plugin pipelines reproducible across PostCSS versions?
35275 reputation · 20 Sept 2023, 11:48 UTC
PostCSS provides no ordering guarantees beyond sequential execution. Plugins run in the exact order registered via use(). The processor does not topologically sort, validate declared dependencies, or detect conflicts. When multiple plugins rewrite the same declaration, the final AST is determined solely by:
Plugin deps metadata is informational only — PostCSS never validates or enforces it. Reproducibility across versions requires explicit plugin ordering in configuration, version locking, and integration tests that snapshot serialized output.
PostCSS walks the AST once per plugin, in registration order. Processor#use pushes plugins onto an array; Processor#process iterates that array and calls each plugin's postcssPlugin function with the root node. There is no dependency graph, no topological sort, and no conflict detection in the core (verified in lib/postcss.js since v6.x through current 8.x).
If Plugin A mutates decl.value in place and Plugin B later replaces the declaration via decl.replaceWith(), Plugin A's change is lost. If the order reverses, Plugin B's replacement becomes the target of Plugin A's mutation. Whitespace and comments stored in node.raws are preserved only on nodes that survive; replacement discards prior raws.
The postcss.plugin(name, { deps: ['other-plugin'] }) option exists for documentation and tooling (e.g., postcss-sort-plugins), but the core processor ignores it. No validation, reordering, or warning occurs.
postcss.config.js, CLI --use, or programmatic use() calls. Do not rely on alphabetical or package-json order.package-lock.json or yarn.lock for PostCSS and all plugins. Core behavior is stable, but plugin internals (e.g., autoprefixer, cssnano) may reorder their internal transforms across major versions.Create a minimal test case: a CSS file with one declaration (color: red), Plugin A that mutates decl.value = 'blue', Plugin B that replaces the declaration with decl.replaceWith(postcss.decl({ prop: 'color', value: 'green' })). Run with both plugin orders via PostCSS CLI and compare output. This confirms the mutation-vs-replacement behavior in your version.
Are any plugins in your pipeline using Root#walk or Container#walk with callbacks that modify the tree during iteration? Such mutations affect traversal order for subsequent plugins and create subtle ordering dependencies beyond simple declaration rewrites.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.