Extract minify into standalone function, deprecate format minify option - #235
Open
bartveneman wants to merge 7 commits into
Open
Extract minify into standalone function, deprecate format minify option#235bartveneman wants to merge 7 commits into
bartveneman 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
Contributor
|
| 📦 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
bartveneman
commented
Jul 23, 2026
- 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
bartveneman
commented
Jul 23, 2026
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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