Skip to content

Add v2 composable CSS analysis pipeline with multiple analyzers#602

Open
bartveneman wants to merge 5 commits into
mainfrom
claude/pensive-johnson-1BGhS
Open

Add v2 composable CSS analysis pipeline with multiple analyzers#602
bartveneman wants to merge 5 commits into
mainfrom
claude/pensive-johnson-1BGhS

Conversation

@bartveneman
Copy link
Copy Markdown
Member

Summary

Introduces a new v2 analysis pipeline architecture that enables composable, tree-shaking-friendly CSS analyzers. The pipeline parses CSS once, walks the AST once, and dispatches nodes to subscribed analyzers via a precomputed dispatch table.

Key Changes

  • Core pipeline (src/v2/core.ts): Implements createPipeline() that accepts a record of analyzer instances, precomputes node-type dispatch tables, and runs a single parse + walk cycle with efficient node routing.

  • Analyzer implementations:

    • uniqueColors: Detects hex colors, named colors, system colors, and color functions while skipping false positives in font-family declarations
    • declarationsPerRule: Counts declarations per style rule
    • linesOfCode: Counts lines in raw CSS (string-based, no AST walk)
    • uniqueMediaFeatures: Extracts and counts media query feature names
    • embeddedContent: Identifies and measures data URIs by MIME type
  • Internal utilities:

    • CountCollection: Efficient counting of unique string values with optional location tracking using string interning and growable Uint32Array
    • NumericCollection: Records numeric samples (e.g., declaration counts) with optional per-sample locations
    • LocationStore: Compact Uint32Array-backed storage for line/column/offset/length tuples
    • StringInterner: Maps strings to dense integer IDs for efficient indexing
    • GrowableUint32Array: Self-expanding Uint32Array for dynamic allocation
  • Comprehensive test suite (src/v2/v2.test.ts): 40+ tests covering all analyzers with and without location tracking, edge cases, and tree-shaking verification.

Notable Implementation Details

  • Memory efficiency: Uses Uint32Array-backed storage instead of heap objects per occurrence, reducing GC pressure for large stylesheets
  • Optional locations: Analyzers support a locations: true option to track source positions without overhead when not needed
  • Tree-shakeable: Analyzers are independent; unused ones don't contribute to bundle size
  • Single-pass: Precomputed dispatch table enables branch-free node routing in the hot loop
  • Type safety: Full TypeScript support with discriminated union types for results with/without locations

https://claude.ai/code/session_01BoXkHszbnfFu3yR2ictcJj

claude added 2 commits May 24, 2026 21:32
…nternals)

Explores a from-scratch rewrite focused on tree-shaking, selective analysis,
and lower memory. Each analyzer is a self-contained module subscribing to AST
node types; the pipeline parses + walks once and dispatches via a precomputed
table. Two demo analyzers (unique colors, declarations-per-rule) cover both
the unique-string-count and numeric-distribution shapes, with locations
toggleable per analyzer.

Internals use a string interner + Uint32Array for counts, and flat Uint32Array
location stores (4 slots per location). Not wired into the public API.

src/v2/
  core.ts                            pipeline + dispatch
  index.ts                           public exports
  v2.test.ts                         14 tests, all passing
  internals/                         interner, growable u32, locations, collections
  analyzers/                         unique-colors, declarations-per-rule
…yzers

Also adds optional prepare(css) hook to AnalyzerInstance, called before the
AST walk, for string-level metrics (linesOfCode, embeddedContent sizeRatio).

- linesOfCode: charCode loop over raw string, no allocations
- uniqueMediaFeatures: CountCollection over MediaFeature.property
- embeddedContent: URL nodes filtered to data: URIs, per-MIME-type
  count+size+optional-locations; sizeRatio requires prepare

32 tests total, all passing.
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 25, 2026

Bundle Report

Changes will increase total bundle size by 4.49kB (5.62%) ⬆️⚠️, exceeding the configured threshold of 5%.

Bundle name Size Change
analyzeCss-esm 84.27kB 4.49kB (5.62%) ⬆️⚠️

Affected Assets, Files, and Routes:

view changes for bundle: analyzeCss-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
index.js 3.25kB 36.43kB 9.78% ⚠️
index.d.ts 954 bytes 15.59kB 6.52% ⚠️
browserhacks-DcFslSJt.js (New) 9.26kB 9.26kB 100.0% 🚀
specificity-KjfJJJUw.js (New) 8.04kB 8.04kB 100.0% 🚀
property-utils-CcCRUoMk.js (New) 2.58kB 2.58kB 100.0% 🚀
atrules-BGhbEuC_.js (New) 2.4kB 2.4kB 100.0% 🚀
utils-CjxPISEy.d.ts (New) 1.97kB 1.97kB 100.0% 🚀
values-DsVfVxq1.d.ts (New) 1.31kB 1.31kB 100.0% 🚀
string-utils-CsY6OsHO.js (New) 1.12kB 1.12kB 100.0% 🚀
property-utils-Y3lwhzHs.d.ts (New) 719 bytes 719 bytes 100.0% 🚀
atrules-BZaOfyEA.d.ts (New) 683 bytes 683 bytes 100.0% 🚀
keyword-set-DmR2M4uh.js (New) 338 bytes 338 bytes 100.0% 🚀
keyword-set-CWnW_5rX.d.ts (New) 287 bytes 287 bytes 100.0% 🚀
browserhacks-BZeV9_0h.js (Deleted) -8.97kB 0 bytes -100.0% 🗑️
specificity-CFXs2D2r.js (Deleted) -8.04kB 0 bytes -100.0% 🗑️
property-utils-B3n9KkA9.js (Deleted) -2.58kB 0 bytes -100.0% 🗑️
atrules-BKCGvlv0.js (Deleted) -2.4kB 0 bytes -100.0% 🗑️
utils-yIUD7vGy.d.ts (Deleted) -1.97kB 0 bytes -100.0% 🗑️
values-DI2FP2Ey.d.ts (Deleted) -1.31kB 0 bytes -100.0% 🗑️
string-utils-C97yyuqE.js (Deleted) -1.12kB 0 bytes -100.0% 🗑️
property-utils-MKX6iGvg.d.ts (Deleted) -719 bytes 0 bytes -100.0% 🗑️
atrules-CXLSfmpu.d.ts (Deleted) -683 bytes 0 bytes -100.0% 🗑️
keyword-set-qSyAMR9o.js (Deleted) -338 bytes 0 bytes -100.0% 🗑️
keyword-set-BXSoLQ6m.d.ts (Deleted) -287 bytes 0 bytes -100.0% 🗑️

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 25, 2026

Codecov Report

❌ Patch coverage is 95.01385% with 54 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.93%. Comparing base (f175af0) to head (d8dc711).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
src/v2/analyzers/colors-context.ts 69.56% 10 Missing and 4 partials ⚠️
src/v2/analyzers/values/animations.ts 72.41% 6 Missing and 2 partials ⚠️
src/v2/analyzers/atrule-misc.ts 78.57% 4 Missing and 2 partials ⚠️
src/v2/internals/location-store.ts 78.26% 5 Missing ⚠️
src/v2/analyzers/properties.ts 94.11% 3 Missing ⚠️
src/v2/analyzers/source-lines-of-code.ts 85.71% 1 Missing and 2 partials ⚠️
src/v2/analyzers/values/color-formats.ts 90.90% 2 Missing and 1 partial ⚠️
src/v2/analyzers/selectors.ts 97.46% 2 Missing ⚠️
src/v2/internals/aggregate-collection.ts 94.73% 2 Missing ⚠️
src/v2/internals/growable-u32.ts 92.00% 2 Missing ⚠️
... and 6 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #602      +/-   ##
==========================================
+ Coverage   94.48%   94.93%   +0.44%     
==========================================
  Files          18       66      +48     
  Lines        1016     2171    +1155     
  Branches      321      640     +319     
==========================================
+ Hits          960     2061    +1101     
- Misses         46       89      +43     
- Partials       10       21      +11     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

claude added 3 commits May 25, 2026 12:16
Adds 30 new analyzer files covering every metric category from the v9
analyze() function. Each analyzer is an independently importable module;
the bundler can tree-shake anything that isn't imported.

New internals:
  aggregate-collection.ts    — min/max/mean/mode/range/sum over uint32 stream
  context-count-collection.ts — CountCollection grouped by a context key (e.g. property name)

Pipeline changes (core.ts):
  - WalkContext { depth, inKeyframes } passed to every visit()
  - Optional on_comment() hook for parse-level comment metadata
  - keyframesDepth tracked centrally so all analyzers share the context

New analyzers:
  Stylesheet:   stylesheetMeta, sourceLinesOfCode
  At-rules:     atruleImports, atruleCharsets, atruleLayers, atruleFontFaces,
                atruleKeyframes, atruleMedia, atruleSupports, atruleContainers, atruleMisc
  Rules:        rules (total, empty, sizes, nesting, selectorsPerRule)
  Selectors:    selectors (specificity, complexity, ids, pseudos, attributes,
                custom elements, combinators, prefixed, a11y, keyframe selectors)
  Declarations: declarations (total, unique, importants, inKeyframes, nesting)
  Properties:   properties (prefixed, custom, shorthands, browserhacks, complexity)
  Values:       gradients, fontFamilies, fontSizes, lineHeights, zIndexes,
                shadows, borderRadii, animations, units, keywords, resets,
                displays, colorFormats, vendorPrefixedValues, valueBrowserhacks

84 tests, all passing.
- Add compat layer (src/v2/compat.ts) wrapping all v2 analyzers to produce
  identical output shape to v9's analyze() function
- Copy v9 test suite as src/v2/compat.test.ts to prove compat parity
- Add missing analyzers: atrule-all, colors-context, value-complexity
- Fix properties analyzer to expose `unique` field in result
- Fix compat: export compareSpecificity as `compare` alias from specificity.ts
- Fix compat: exclude prefixedRatio from atrules.keyframes spread
- Fix compat: use statsFromItems() for atrules.nesting (was flatAggColl cast)
- Fix singleton pipeline bug: use makePipeline() factory to prevent state leakage

All 24 compat tests and 84 v2 unit tests pass.
…olor skip set

WalkContext: reuse one mutable object across the whole walk instead of
allocating a new {depth, inKeyframes} per node. For a large stylesheet
with thousands of nodes this eliminates tens of thousands of short-lived
heap objects.

Dispatch table: replace Map<nodeType, handlers> with a flat array indexed
by node type (small dense integers). Array index access beats Map.get()
for the integer key case.

Keyframes guard: only call is_atrule() + toLowerCase() + endsWith() when
not already inside a @Keyframes block, cutting that work to zero for all
nodes inside keyframes.

AggregateCollection: track min/max at push() time (two scalar comparisons)
instead of sorting the full buffer. collect() now only sorts once — a
typed-array slice with no comparator function (numeric sort, faster than
Array sort with (a,b)=>a-b) — and allocates Array.from(view) once instead
of twice.

StringInterner: drop the parallel strings[] array. The reverse id→string
lookup is built as a side effect of the unique-record loop in
CountCollection.collect(), so no extra allocation or iteration.

CountCollection: bump GrowableUint32Array initial capacity 32→64 to
reduce early doublings. Per-value LocationStore initial capacity 4→8
(via explicit argument) to halve first-grow frequency for duplicate values.

LocationStore: bump default initial capacity 4→8 (2 locations before
first resize instead of 1).

colors-context: expand SKIPS_COLOR_LOOKUP from 2 entries to ~80 entries
covering layout, spacing, typography, transforms, animation, flex/grid,
table, and other properties whose values can never contain color tokens.
Skipping the inner value walk for these saves work proportional to their
frequency in real stylesheets.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants