Extract minify into standalone function, deprecate format minify option#235
Extract minify into standalone function, deprecate format minify option#235bartveneman wants to merge 7 commits into
Conversation
minify() is now its own module that walks the CSS token stream
directly (no parser/AST at all) and gets its own bundle entry
(@projectwallace/format-css/minify), so importing it no longer pulls
in format()'s pretty-printer.
This is non-breaking: FormatOptions.minify and the matching { minify }
option on format_value, format_declaration, format_selector,
format_selector_list and format_atrule_prelude keep working exactly as
before, just marked @deprecated - format(css, { minify: true })
delegates straight to the new minify() under the hood. Removing the
deprecated option entirely is left for a later major version.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWaxL8XDQmReNLkPfytrU1
format() no longer branches on minify (it delegates to minify() up front instead), so these were always just aliases for SPACE and SEMICOLON. Inline them, and drop the now-always-true is_last ternary they fed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YWaxL8XDQmReNLkPfytrU1
|
| 📦 Package | 📏 Base Size | 📏 Source Size | 📈 Size Change |
|---|---|---|---|
| @projectwallace/format-css | 8.7 kB | 10.3 kB | +1.6 kB |
tsc's "Check types" job runs on a fresh checkout, before any build step, so package.json's ./minify export (which points at dist/minify.d.ts) can't resolve yet - only worked locally because dist/ happened to already exist from a prior build. Add a paths mapping to the source file, mirroring the existing one for the bare package specifier. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YWaxL8XDQmReNLkPfytrU1
| } | ||
|
|
||
| let printed = print_selector(selector, OPTIONAL_SPACE) | ||
| let printed = print_selector(selector, SPACE) |
There was a problem hiding this comment.
Should not pass space param here. Print selector should have this as i ternal knowledge.
There was a problem hiding this comment.
Fixed in 60768e1 - print_selector() already defaults optional_space to SPACE, so the explicit argument was redundant.
Generated by Claude Code
| * Adapted from the pretty-printer's at-rule prelude formatter, hardcoded to | ||
| * always minify (at-rule preludes are matched as raw text, not tokenized | ||
| * into a real value tree, so this text-based approach is reused as-is). | ||
| */ |
There was a problem hiding this comment.
Is this a shortcoming of the css parser? It sounds really weird.
There was a problem hiding this comment.
Not a parser limitation - @projectwallace/css-parser can fully parse at-rule preludes into a tree via parse_atrule_preludes: true. format() just passes false and formats preludes as raw text via regex instead (see the comment above format_atrule_prelude - the combinatorial grammar across @media/@supports/@container/etc. wasn't judged worth a full tree for this formatter). My comment wording made it sound like a parser shortcoming; reworded in 60768e1.
Generated by Claude Code
- print_rule_selectors: drop the redundant explicit SPACE argument to print_selector() - it already defaults to SPACE, and format() never varies it, so the caller doesn't need to know that detail. - Correct a misleading comment in minify_atrule_prelude(): the css parser can parse at-rule preludes into a real tree (parse_atrule_preludes: true) - format()'s own pretty-printer just chooses not to use that and formats preludes as raw text via regex instead. It's format-css's design choice, not a parser limitation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YWaxL8XDQmReNLkPfytrU1
Replace the recursive-descent token walker (statement/rule/declaration disambiguation, separate selector/value printing modes, nth-child micro-parsing, attribute-selector handling, a context stack) with one flat loop over the token stream. It tracks only the previous token's last character and applies one rule everywhere: a fixed set of characters (`, : ; ! > < = ~ /` plus grouping punctuation) always drop their surrounding space since they're unambiguous in any CSS context; everything else keeps exactly one space if the source had one. Two small lookbehind checks handle the remaining correctness-relevant cases (an empty custom-property value's "space toggle", and a redundant `;` right before a block closes). `+`, `-` and `*` are deliberately left out of the always-strip set: they're also used where whitespace is significant (calc's mandatory `+`/`-`, the universal selector `*` before a descendant combinator), and disambiguating those would require knowing what's being printed - exactly the state this rewrite removes. This means a few spots (`calc(1px * 2)`, `:nth-child(-n + 3)`, `a + b`) keep an optional space a fussier minifier would strip; updated the three tests that pinned that stripping to reflect the new, deliberately-not-maximal behavior. 85 lines total, no recursion, no allocated token array (streams the generator directly). dist/minify.js drops from 12.5 kB to 2.5 kB (1.26 kB gzip), and dist/index.js (which re-exports it) from 27.8 kB to 17.7 kB. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YWaxL8XDQmReNLkPfytrU1
, : ; ( ) [ ] { } each have their own dedicated token type, so
checking type (a number) is both cheaper and clearer than slicing the
source into a 1-char string and hashing it through a Set<string>. Only
the remaining delimiter characters (! / < = > ~ + - *) share a single
DELIM token type and still need a char-level check, but that's now a
numeric charCodeAt comparison instead of a string.
No behavior change - same 263 tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWaxL8XDQmReNLkPfytrU1
| if (type === TOKEN_RIGHT_BRACE && prev_type === TOKEN_SEMICOLON) { | ||
| out = out.slice(0, -1) | ||
| } | ||
|
|
There was a problem hiding this comment.
This is causing garbage collection a lot to clean up temp strings. Look for better implementation, or else just skip it.
There was a problem hiding this comment.
Fixed in 74c045c - the ; is now never written eagerly. It's held as a pending_semicolon flag and only flushed once the following token is known, dropped entirely if that's }. No more write-then-slice(0,-1) re-copy of the whole accumulated output per rule. Verified on a 1.2MB/20k-rule synthetic stylesheet (~156ms, no quadratic blowup) plus the full test suite.
Generated by Claude Code
out.slice(0, -1) re-copies the whole accumulated output every time a rule ends with `;` before `}` - on a large stylesheet that's an O(n) copy per rule, so effectively O(n^2) overall and a lot of short-lived garbage. Defer writing `;` at all instead: hold it as a pending flag, write it only once we see what follows, and just never write it if that's a closing brace. A `;` at the very end of the input (nothing following it to trigger the flush) needed one more line to flush after the loop. Verified on a 1.2MB/20k-rule synthetic stylesheet (~156ms, no quadratic blowup) and against the full test suite. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YWaxL8XDQmReNLkPfytrU1
Summary
This PR refactors the minification logic into a standalone, token-stream-based minifier that doesn't build an AST. The
minify()function is now a separate implementation rather than a wrapper aroundformat(), and theminifyoption onformat()and related functions is deprecated in favor of usingminify()directly.Key Changes
New standalone minifier (
src/lib/minify.ts): Implements CSS minification by walking the token stream directly, dropping comments and unnecessary whitespace without building a syntax tree. This is more efficient than the previous approach of parsing and then formatting with minification flags.Refactored
format()function:minifyoption is now deprecated (marked with@deprecatedJSDoc)minify: trueis passed, it delegates to the standaloneminify()functionOPTIONAL_SPACE,LAST_SEMICOLON, and conditional branches)Updated exports:
minify()is now exported fromsrc/lib/minify.ts./minifyinpackage.jsonto allow tree-shaking (users can import just the minifier without the formatter)minifyfrom main index for backwards compatibilityUpdated documentation (README.md):
minify()is a standalone function with different behavior thanformat()minifyoption onformat()is deprecatedTest updates: Updated test descriptions to note deprecated options, and changed tests to use
minify()directly instead offormat(..., { minify: true })Build configuration: Updated
tsdown.config.tsto build the minify module separately and configuredvitest.config.tswith path aliases for the new subpath exportImplementation Details
The new minifier uses a two-pass approach:
space_beforeflagsThis approach is more efficient than building a full AST and allows for precise control over which whitespace is semantically meaningful.
https://claude.ai/code/session_01YWaxL8XDQmReNLkPfytrU1