Skip to content
adobePublic
forked from reworkcss/css

About

CSS parser / stringifier for Node.js

Resources

Code of conduct

Contributing

Security policy

Stars

87 stars

Watchers

3 watching

Forks

 
 

Repository files navigation

@adobe/css-tools

A modern CSS parser and stringifier with TypeScript support

npm version License: MIT

Parse CSS into an Abstract Syntax Tree (AST) and convert it back to CSS with configurable formatting. Built with TypeScript for type safety and modern JavaScript features.

V5: Final Compatibility Release

V5 will be the final release of @adobe/css-tools. We recommend moving to PostCSS directly whenever possible, especially for new projects.

V5 retains V4's public API and css-tools AST. It replaces the parser internals with PostCSS and adds compatibility shims for existing options and default output. The returned AST remains a css-tools AST rather than a native PostCSS AST. The V4 parser is not used at runtime.

Compatibility is verified for the tested corpus, not every CSS input. The upstream parser limitations are accepted for this final release and will not be fixed in css-tools.

Using PostCSS directly is a separate migration: its API and AST differ from css-tools. V5 lets existing applications upgrade without that migration while keeping a path open to using PostCSS directly.

Install

npm install @adobe/css-tools

Usage

import { parse, stringify } from '@adobe/css-tools'

// Parse CSS to AST
const ast = parse('body { font-size: 12px; }')

// Stringify AST back to CSS
const css = stringify(ast)
// => "body { font-size: 12px; }"

// Pretty print with custom indentation
const formatted = stringify(ast, { indent: '  ' })
// => "body {\n  font-size: 12px;\n}"

// Minify output
const minified = stringify(ast, { compress: true })
// => "body{font-size:12px}"

API

parse(code, options?)

Parses CSS code and returns an Abstract Syntax Tree (AST).

Parameters:

  • code (string) - The CSS code to parse
  • options (object, optional) - Parsing options
    • silent (boolean) - Collect recoverable parse errors instead of throwing. Recovery can still throw for some malformed inputs; see known issues.
    • source (string) - File path for better error reporting
    • stripCommentsInValues (boolean) - Remove comment-like text from declaration values, including inside quoted strings, for V4 compatibility (default: true). Set to false to preserve leading, embedded, and trailing value comments.

Returns: CssStylesheetAST - The parsed CSS as an AST

stringify(ast, options?)

Converts a CSS AST back to CSS string with configurable formatting.

Parameters:

  • ast (CssStylesheetAST) - The CSS AST to stringify
  • options (object, optional) - Stringification options
    • indent (string) - Indentation string (default: ' ')
    • compress (boolean) - Whether to compress/minify the output (default: false)

Returns: string - The formatted CSS string

Features

  • Complete CSS Support: All standard CSS features including selectors, properties, values, at-rules, and comments
  • TypeScript Support: Full type definitions for all AST nodes and functions
  • Error Handling: Configurable error handling with detailed position information
  • Formatting Options: Pretty print, minify, or custom formatting
  • Performance Optimized: Efficient parsing and stringification for large CSS files
  • Source Positions: Track original source positions for debugging and tooling

Supported CSS Features

  • Selectors: Element, class, ID, attribute, pseudo-class, pseudo-element selectors
  • Properties: All standard CSS properties and custom properties
  • Values: Colors, lengths, percentages, functions, calc(), etc.
  • At-rules: @media, @keyframes, @import, @charset, @namespace, @font-face, @page, @document, @supports, @container, @layer, @starting-style, @host, @custom-media
  • Comments: Both /* */ and // comments
  • Whitespace: Preserves formatting information
  • Vendor prefixes: Supports vendor-prefixed at-rules and properties
  • Nested rules: Media queries, supports, containers, etc.
  • Complex selectors: Combinators, pseudo-selectors, attribute selectors

Examples

Error Handling

import { parse } from '@adobe/css-tools'

const malformedCss = `
  body { color: red; }
  { color: blue; } /* Missing selector */
  .valid { background: green; }
`

// Parse with silent error handling
const result = parse(malformedCss, { silent: true })

// Check for parsing errors
if (result.stylesheet.parsingErrors) {
  console.log('Parsing errors:', result.stylesheet.parsingErrors.length)
  result.stylesheet.parsingErrors.forEach(error => {
    console.log(`Error at line ${error.line}: ${error.message}`)
  })
}

// Valid rules are still parsed
console.log('Valid rules:', result.stylesheet.rules.length)

Source Tracking

import { parse } from '@adobe/css-tools'

const css = 'body { color: red; }'
const ast = parse(css, { source: 'styles.css' })

// Position information is available
const rule = ast.stylesheet.rules[0]
console.log(rule.position?.source) // "styles.css"
console.log(rule.position?.start) // { line: 1, column: 1 }
console.log(rule.position?.end) // { line: 1, column: 20 }

Positions and parse errors refer to the supplied CSS. As in V4, input source maps are not loaded or resolved; sourceMappingURL comments remain ordinary CSS comments.

For more examples, see the Examples documentation.

Performance

V5's PostCSS-based parser was faster than the published V4.5.0 parser on these samples. Measurements used Benchmark.js with repeated warm, steady-state samples on Node.js 24.18.0 and an Apple M3 Max (macOS/arm64); each figure is the median of three runs. ASTs and stringified outputs were checked for equality before timing. These measurements use PostCSS 8.5.30 and include the final compatibility repairs.

CSS input Size V4.5.0 parse (ms) V5 parse (ms) V5 speedup
One rule 20 bytes 0.0057 0.0051 1.1x
benchmark/small.css 7.8 KB 0.477 0.336 1.4x
Pure.css 3.0.0 26.2 KB 4.140 1.662 2.5x
Bootstrap 5.3.3 281 KB 621.3 24.8 25.1x

For Bootstrap, parsing plus default stringification improved from 633.4 ms to 27.7 ms, about 23x faster. Stringification alone was broadly unchanged: Bootstrap took about 0.76 ms in both V4 and V5.

Warm throughput is not cold-start latency, and small inputs do not always favor V5. Separate alternating-order, forced-GC batch runs measured a 17-byte rule at 0.0061 ms in V4 and 0.0127 ms in V5, and benchmark/small.css at 0.521 ms in V4 and 0.591 ms in V5. Median module loading, excluding process startup, was 0.65 ms for V4's CJS entry and 8.87 ms for V5's ESM entry across eleven fresh processes each; the first parse after loading took 0.60 ms and 3.05 ms. Fresh-process Bootstrap peak RSS was about 72.1 MiB in V4 and 97.9 MiB in V5, including module initialization, a live AST, and compressed stringification. The standalone UMD bundle grew from 20,942 to 131,834 bytes, or from 4,701 to 39,296 bytes gzipped.

These are sample measurements, not guaranteed speedups. Results depend on the CSS, warm-up, garbage collection, runtime, and hardware. See the benchmark/ directory for the existing parser benchmark and its inputs, and known issues for upstream adversarial-input costs.

V4 Compatibility Tests

The V5 parser uses PostCSS with compatibility shims, not the V4 parser. css-tools-v4 is a development-only alias pinned to the published @adobe/css-tools@4.5.0 release as the test oracle.

npm ci
npm test -- --runInBand
npm run build
npm run test:bundles
npm run test:compatibility:public

The offline suite compares all existing CSS fixtures, lexical/error edge cases, and 1,000 seeded generated inputs. It checks complete AST fields, source positions, non-enumerable parent links, strict and silent errors, and byte-identical default, compressed, and custom-indent output. It also checks that V5 can stringify V4 ASTs. Bundle checks cover CJS, ESM, and standalone UMD browser, CommonJS, and AMD loading. Each loader also parses depth-2048 nested media in an isolated process with a 128 MiB old-space limit and a five-second deadline, checking ASTs, positions, and parent descriptors without recursively walking the result. This gate covers parsing, not deep stringification.

The public-CSS command samples eight complete stylesheets from ten pinned npm distribution URLs on jsDelivr, using seed 0x405000. It verifies SHA-256 checksums before testing and stores downloads and their provenance in the gitignored .compatibility-css/ directory. The sample includes Bootstrap (normal and minified), Bulma, Animate.css, Pure.css, Milligram, Water.css, and Modern Normalize. Public files use default parse options and all three formatting modes; the offline inputs exercise source and silent combinations. The published V4 parser is slow on the full Bulma file, so this opt-in command can take several minutes.

Passing these comparisons establishes exact parity for the tested inputs, not a proof of identical behavior for every possible CSS string. Additional lexical and malformed-input probes have confirmed differences outside that corpus; see known issues.

Known Issues in V5

The following upstream resource limitations are accepted and will not be fixed in css-tools. They affect both strict and silent parsing.

Core PostCSS at-rule token handling takes superlinear time on headers with many leading comments, such as '@unknown ' + '/**/ '.repeat(n) + 'x;'. PostCSS repeatedly removes tokens from the front of an array before the compatibility layer receives the AST. This is not regex backtracking.

Core PostCSS unclosed url( tokenization searches the rest of the input for a closing parenthesis at each unclosed url(, so input with many unclosed url( takes quadratic time. To restore V4's recovery, V5's silent mode can parse such input up to eight more times, which multiplies this cost:

  • On 384 KB inputs like 'a{b:' + 'url(x '.repeat(n) + '}', PostCSS ran two or three times, and V5 reported about 320,000 errors, V4's error for each character V4 skips. Parsing took 1.2–2.5 s.
  • On 688 KB of 'a{b:url(x;}'.repeat(n), PostCSS ran nine times, and parsing took about 5 s.

postcss-safe-parser alone took about 0.23 s and 0.44 s on those inputs. V4's silent mode took over 4 s on 12 KB of the first shape and 28 s on 43 KB of the second.

Safe-parser recovery resource exhaustion occurs when postcss-safe-parser recursively recovers long sequences without declaration semicolons, such as 'a {' + '\n color: red'.repeat(3100) + '\n}'. Each recursion level copies the remaining tokens, so this can overflow the stack or exhaust memory before the compatibility layer receives an AST. With Node 24's default limits, 37 KB of this shape threw a stack-overflow RangeError after about 0.15 s, 300 KB took about 1.2 s and 3.4 GB of memory to throw, and 600 KB exhausted the heap, which aborts the process. V4 parses each in under 5 ms. The thresholds depend on the runtime and resource limits. This recovery issue is separate from core PostCSS's token-handling cost.

For untrusted CSS, silent is not an exception-safety or execution-time guarantee. Limit input size and isolate parsing in a worker or process with an enforceable timeout and memory limit, such as a Worker with resourceLimits; an in-process timer cannot interrupt synchronous parsing, and heap exhaustion cannot be caught. Applications relying on V4's behavior for these inputs should not treat V5 as a behaviorally identical upgrade.

Separately, some malformed CSS still parses differently from V4, in strict errors, silent recovery, or both. Known examples:

  • After eight re-parses, silent mode keeps PostCSS's reading of any further malformed input. With nine or more copies of a{b:(c;}d{e:f}, V5 keeps fewer rules than V4. With nine or more copies of a{b:url(x;}, it keeps url(x values where V4 reports errors.
  • V4 reads a nested block at-rule's prelude past the parent's } into a later block, as in @media y{@media x}b{}}; V5 keeps PostCSS's structure. At the root, V4 also reads some at-rules' preludes past a stray }, as in @foo x}b{c:d}; V5 reads the later block as a separate rule.
  • V4 accepts unclosed [ in values and selectors, as in a{b:[c;}d{e:f} and a[b{}.
  • When no ) follows an unclosed url( in a selector, as in url(x {}, V4 skips the selector text through the (; V5 keeps it and reports PostCSS's Unclosed bracket error.
  • V4 rejects a block after an @import nested in a block at-rule, as in @media x{@import y{a:b}}; V5 reads an @import rule.
  • V4 ends a @keyframes block at a nested at-rule or selectorless block, as in @keyframes x{@media x{}} and @keyframes x{{}}, reports a missing } and reads the rest outside the block. V5 keeps the @keyframes block and drops the nested content, reporting an error only for malformed content, such as a missing selector.

Documentation

Background

This is a fork of the npm css package, maintained by Adobe with modern improvements including TypeScript support, enhanced performance, and security updates. It provides a robust foundation for CSS tooling, preprocessing, and analysis.

License

MIT

About

CSS parser / stringifier for Node.js

Resources

Code of conduct

Contributing

Security policy

Stars

87 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages