feat(core): only-include-used-components: opt-in trimming of the spacing utility grid (~78 kB raw / ~11.5 kB gzip) - #509
Open
kevbarns wants to merge 2 commits into
Open
feat(core): only-include-used-components: opt-in trimming of the spacing utility grid (~78 kB raw / ~11.5 kB gzip)#509kevbarns wants to merge 2 commits into
kevbarns wants to merge 2 commits into
Conversation
…ing utility grid Closes #508 With component trimming in place (#505), the floor of the stylesheet is the core, of which ~78 kB raw (~11.5 kB gzip) is the exhaustive fr-m*/fr-p* spacing grid (1215 rules, 2457 classes in dsfr 1.14.2). These classes are never toggled by the DSFR runtime, so they can be trimmed per rule. Since this is the script's first rule level surgery, it ships behind a separate opt-in flag, --trim-spacing-utilities, and the risky part happens at react-dsfr build time: generateSpacingUtilitiesManifest cross-checks the string level extractor against a real CSS parser (a devDependency) on the exact files that get published, and fingerprints them in a generated dsfr/core/spacing-utilities.json. At run time, trimming only happens when the core file's hash matches the manifest; on any mismatch the stylesheet ships untrimmed, with a never-silenced warning (non zero exit in --strict). Used utilities are detected as literal class names in the already crawled sources, plus the build time derived list of utilities react-dsfr's own components render (the crawl excludes the package). Dynamically constructed class names are detected by their static prefix and every utility the prefix can produce is kept: `fr-m${x}` keeps the margins but trims the paddings, `fr-icon-${x}` triggers nothing. The escape hatch is "react-dsfr"."additionalSpacingUtilities" (["fr-mt-2w", "fr-mb-*"]), where a * suffix declares a prefix and acknowledges the matching dynamic construction under --strict. Measured on a Vite-shaped app using Button and two utilities: dsfr.min.css goes from 231.9 kB raw / 31.8 kB gzip to 154.4 kB / 20.4 kB. Without the flag the output is byte identical to the previous release.
There was a problem hiding this comment.
Pull request overview
Adds an opt-in (--trim-spacing-utilities) to only-include-used-components that trims unused DSFR spacing utility rules (fr-m* / fr-p*) from the core stylesheet, using a build-time generated manifest + runtime hash-based fail-safes to avoid unsafe rule-level purging.
Changes:
- Introduces runtime extraction/detection/trimming utilities for spacing rules, including fail-safe “do nothing” behavior on manifest/hash/count mismatches.
- Adds a build-time generator that validates the string-level extractor against a real CSS parser and emits
dsfr/core/spacing-utilities.json. - Updates the CLI + documentation and adds a comprehensive test suite for trimming behavior, detection edge cases, and manifest invariants.
Reviewed changes
Copilot reviewed 8 out of 11 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
src/bin/trimSpacingUtilities.ts |
Implements spacing rule extraction, used token detection, dynamic-prefix detection, and core CSS trimming with fail-safes. |
src/bin/only-include-used-components.ts |
Wires --trim-spacing-utilities into the CLI flow, config escape hatch parsing, warnings, and --strict behavior. |
scripts/build/generateSpacingUtilitiesManifest.ts |
Build-time manifest generator that cross-checks extraction vs a CSS parser and fingerprints core variants. |
scripts/build/build.ts |
Integrates manifest generation into the build output under dsfr/core/spacing-utilities.json. |
src/bin/README.md |
Documents spacing trimming behavior, safety model, and additionalSpacingUtilities escape hatch. |
test/runtime/scripts/onlyIncludeUsedComponents/trimSpacingUtilitiesFromCoreCss.test.ts |
Unit tests for trim behavior, partitioning correctness, and fail-safe paths. |
test/runtime/scripts/onlyIncludeUsedComponents/spacingUtilitiesManifest.test.ts |
Tests manifest generator assumptions against installed DSFR and pins expected counts. |
test/runtime/scripts/onlyIncludeUsedComponents/generateDsfrCssCode.test.ts |
Ensures trimming integrates correctly with DSFR CSS generation and transformation ordering. |
test/runtime/scripts/onlyIncludeUsedComponents/extractSpacingCssRules.test.ts |
Tests extraction correctness and safety against false-positive rule shapes. |
test/runtime/scripts/onlyIncludeUsedComponents/detectUsedSpacingTokens.test.ts |
Tests literal token detection behavior, including intended over-inclusion cases. |
test/runtime/scripts/onlyIncludeUsedComponents/detectDynamicSpacingClassPrefixes.test.ts |
Tests dynamic construction prefix detection and near-miss behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
A corrupted spacing-utilities.json (invalid JSON or unexpected shape) used to surface as a raw stack trace, unlike the corrupted-core case which warns and ships untrimmed. parseSpacingUtilitiesManifest now shape-checks the file and a malformed manifest takes the same path as a missing one: never-silenced warning, untrimmed stylesheet, non zero exit under --strict. Also log that "additionalSpacingUtilities" has no effect when the flag is not passed (a forgotten flag made the configuration a silent no-op), and document in the README exactly which two dynamic class construction forms are detected (template literal interpolation and + concatenation).
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.
Closes #508. Same architecture and fail-safe philosophy as #505/#506.
What this does
--trim-spacing-utilities, a separate opt-in ofonly-include-used-components, removes from the core stylesheet thefr-m*-*/fr-p*-*utilities the project's sources never use. Measured on a Vite-shaped app importingButtonand using two utilities (fr-mt-4w,fr-p-2v):--trim-spacing-utilities1209 of the 1215 spacing rules are removed (the used tokens, their grouped aliases, and the 4 utilities react-dsfr's own components render are kept). Without the flag the output is byte identical to the current release (verified by rebuilding
mainand comparing).How it stays safe
This is the script's first rule level surgery (the spacing grid lives inside
core.*.css, there is no granular file to include or exclude), so the invariant of #505 does not apply and the risky part is moved to react-dsfr's build time, where thecssparser (a devDependency, unavailable in the consumer-side bin) exists:scripts/build/generateSpacingUtilitiesManifest.tscross-checks the string level extractor against the real parser on the exact files that get published — grammar exhaustiveness (a wider regex net must not catch anything the strict grammar misses), zero mixed rules, rule text uniqueness, identical token sets across the 4 core variants. Any violated assumption is a build failure, so a@gouvfr/dsfrbump cannot drift silently. The same asserts run in CI against the installed DSFR (spacingUtilitiesManifest.test.ts, hard failure whennode_modulesis absent, no skip).dsfr/core/spacing-utilities.json. At run time, trimming only happens when the core file's hash matches; on any mismatch (patched file, drift) the stylesheet ships untrimmed with a never-silenced warning, and--strictexits non zero.fr-mt-1v,fr-mb-2v/4v/6vtoday), derived at build time fromsrc/— the crawl deliberately excludes the package, and this list is generated, never written by hand.Dynamic class names
One deviation from the issue text: instead of falling back to the whole family, a dynamically constructed class (
`fr-mt-${x}w`,"fr-m" + side) keeps every utility its static prefix can produce. Same fail-safe guarantee, strictly finer:`fr-m${x}`keeps the margins but still trims the paddings, a bare`fr-${x}`degenerates into keeping the grid, and`fr-icon-${name}`or`fr-modal-title-${id}`trigger nothing because the prefixes are tested against the actual token set extracted from the installed CSS. Each uncovered dynamic construction is a never-silenced warning naming the file and the remedy, and fails--strict.The escape hatch mirrors
additionalComponents:A
*suffix declares a prefix and acknowledges the dynamic constructions it covers (that is how--strictpasses when the dynamic class is intended). Unknown values warn and disable spacing trimming for the run.Tests
The false positive cases were written first, per dimension: mentions in urls/comments (counted on purpose, and pinned as such), maximal-run near misses (
fr-mt-2vfoo), dynamic near misses (fr-modal-title-${id}), grouped selectors kept whole when one token is used, exact partition (trimmed output + removed rules reassemble the input byte for byte), hash/count mismatch fail-safes. The build time cross-check was verified non vacuous by re-introducing a grammar bug (dropping negative value support) and a scanner bug, both fail loudly.End to end, on a throwaway project exercising the shipped bin: nominal trim,
--strictin both directions, escape hatch coverage, one-byte core corruption (warn + untrimmed output), idempotency (No change since last run), and--silentnot hiding any degradation warning.