Skip to content

feat(core): Add only-include-used-components: opt-in trimming of unused DSFR component CSS - #505

Merged
kevbarns merged 7 commits into
codegouvfr:mainfrom
kevbarns:feat/only-include-used-components
Aug 19, 2026
Merged

feat(core): Add only-include-used-components: opt-in trimming of unused DSFR component CSS#505
kevbarns merged 7 commits into
codegouvfr:mainfrom
kevbarns:feat/only-include-used-components

Conversation

@kevbarns

@kevbarns kevbarns commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Add only-include-used-components: opt-in trimming of unused DSFR component CSS

Closes #304 (or at least addresses its main pain point)

Problem

dsfr.min.css weighs ~600kB raw / ~76kB gzip and is loaded render-blocking, while most apps use a small subset of the DSFR components. On Lighthouse mobile audits this is consistently flagged as the main "reduce unused CSS" offender (~95% unused on our app, La Bonne Alternance).

Classic PurgeCSS-style tree shaking is not safe here because the DSFR JS adds classes and attributes at runtime (data-fr-js-*, fr-collapse--expanded, ...), as discussed in #304.

Approach

A new opt-in script, only-include-used-components, modeled after only-include-used-icons (same CLI ergonomics: --projectDir, --silent, same project/public dir discovery, same cache clearing, same idempotence).

Instead of purging individual rules, it rebuilds dsfr/dsfr.css and dsfr/dsfr.min.css in node_modules (and public/dsfr/dsfr.min.css + index.html hash busting for SPAs) by concatenating the granular stylesheets already shipped in the package (dsfr/core/*, dsfr/scheme/*, dsfr/component/<name>/*, including the print variants):

  • Whole components are included or excluded, never individual rules. A component's stylesheet is kept in full, so everything the DSFR JS can toggle at runtime keeps working.
  • Core and scheme (dark mode palette) are always included.
  • The upstream section order is preserved (constant DSFR_COMPONENTS_CASCADE_ORDER). The upstream bundle additionally groups rules by media context globally (all unmediated rules from every component, then all @media (min-width: 36em), ...) while the concatenation groups them by component, which displaces about 29% of the rules. That reordering is inert here: no selector is declared with divergent values in two different granular files, so there is no equal-specificity conflict whose winner could flip.
  • Relative url(...) asset paths are rewritten from the granular file location to the dsfr/ root, so fonts and icons keep resolving, and the assets the regenerated stylesheet references are copied into public/dsfr (SPA setups), the way only-include-used-icons does for icons.
  • The Mui compat patch (:not([class^="Mui"]) on button:not(:disabled):hover/active, cf scripts/build/patchCssForMui.ts) is reapplied to the core chunk, string-based so the script needs no new runtime dependency.

I validated the reconstruction empirically against the shipped bundle: with all components selected, core.main.css + scheme.css + component/*/*.main.css + print variants covers 100% of the rules of dsfr.main.css + dsfr.print.css (the only structural difference is the dark-mode custom properties being declared in two blocks instead of one merged block).

Detection of used components

  1. Imports: an @codegouvfr/react-dsfr/<Module> specifier in an actual import statement (from "...", import "...", import("..."), require("..."), @import "..."), resolved through a static table REACT_DSFR_MODULE_TO_DSFR_COMPONENTS that includes transitive dependencies (e.g. Header → header, navigation, modal, logo, button, link, search, input, form). The table was built by extracting the fr-* classes each component (and its internal imports) renders, and mapping them to the owning DSFR stylesheet. When in doubt, a dependency is included (too much CSS is a size cost, not enough is a rendering bug).
  2. Raw class names: a small static table of root class prefixes per component (fr-table → table, fr-btn → button, ...) catches fr.cx("fr-table") / plain JSX class usage without the React component. Stylesheets are deliberately not scanned: this detection is substring based, so a single compiled bundle (a leftover out/, a dependency shipping the DSFR) would mark every component as used.
  3. Config escape hatch for anything the detection cannot see (classes built dynamically, CMS content, components only referenced from a stylesheet...):
    // package.json
    "react-dsfr": {
        "additionalComponents": ["table", "Range"]
    }
    (accepts DSFR CSS component names or react-dsfr component names)

Fail-safe: if the sources import a react-dsfr module the static table does not know (e.g. a component added in a newer release), the script warns and includes every component — output equivalent to the original bundle, never a broken page. Since that fallback exits 0 and looks like a success in CI, --strict turns it into a failure instead.

What is tested

  • 28 unit tests (test/runtime/scripts/onlyIncludeUsedComponents/): import detection (default/named/deep/require/dynamic imports, blocks/, direct dsfr/component/* css imports, and negative cases for urls and comments mentioning the package), module resolution (components, non-components, unknown → fail-safe), raw class detection, asset extraction, stylesheet generation (cascade order, exclusion, url rewriting, charset/sourcemap stripping, Mui patch, main→plain css fallback for components like download, determinism).
  • DSFR_COMPONENTS_CASCADE_ORDER is asserted against the section order extracted from the sources of the installed @gouvfr/dsfr/dist/dsfr.main.css.map, so it cannot silently drift on a DSFR bump.
  • Full suite passes: 25 files / 103 tests, yarn build OK, eslint + prettier clean.
  • Manual integration test on a throwaway Vite-like project importing Button, Alert, Accordion + "additionalComponents": ["table"]:
    • dsfr.min.css: 600kB → 282kB raw (-53%), 76kB → 36kB gzip (-52%) with 5/45 components. Most of the remainder is the core (typography, grid, color tokens, Marianne font-faces), which is incompressible without breaking things.
    • excluded components (fr-header, fr-footer, fr-tabs...) absent from the output, core/scheme/print/fr-grid-row present, no url("../...") left, Mui patch applied twice (hover + active).
    • idempotent: second run prints No change since last run, and a run after a fail-safe run correctly restores the trimmed output.
    • SPA path: public/dsfr/dsfr.min.css patched, index.html href gets ?hash=<fnv1a>, and every asset the regenerated stylesheet references is present on disk (0 missing out of 38 urls after growing the component set from Button to Header).

Usage

npx react-dsfr only-include-used-components
# or the standalone bin
npx only-include-used-components

Typically as a prebuild/predev step, next to update-icons. In SPA setups run copy-static-assets before it:

"scripts": {
    "predev": "react-dsfr copy-static-assets && react-dsfr update-icons && react-dsfr only-include-used-components",
    "prebuild": "react-dsfr copy-static-assets && react-dsfr update-icons && react-dsfr only-include-used-components --strict"
}

Documented in src/bin/README.md, which now covers the three bin scripts.

Fail-safe hardening (follow-up commits)

Two paths of resolveModuleIdToDsfrComponents used to return [] ("not a component") instead of undefined ("unknown, include everything"): a direct dsfr/component/<x> stylesheet import for an <x> unknown to DSFR_COMPONENTS_CASCADE_ORDER, and any unrecognized lowercase-starting module id. Both are now undefined.

In the same vein, link and shared were listed as non-component modules while they do render DSFR markup (src/link.tsx renders fr-link, src/shared/Fieldset.tsx renders fr-fieldset, fr-label, fr-hint-text, fr-radio-rich), so they resolved to [] and suppressed the fail-safe rather than triggering it. They are now mapped to the components they render.

Known limitations

  • The two static tables must be maintained when components are added (the fail-safe makes forgetting harmless: the CSS just stops being trimmed for projects using the new component, with a console warning inviting to report it, or a hard failure under --strict).
  • Like only-include-used-icons, detection is textual: dynamically composed import paths or class names are not seen — that is what additionalComponents is for.
  • The trimmed dsfr.min.css concatenates the upstream-minified granular files instead of re-minifying the whole bundle with the css package, so its formatting differs slightly from the original (semantically identical).
  • utility/colors and utility/icons are not part of dsfr.css upstream and are left untouched (icons are already handled by only-include-used-icons).
  • If public/dsfr exists but no index.html can be found (a monorepo run with --projectDir, a project that lost its index.html), the script crashes on a message-less AssertionError. This is pre-existing and identical in only-include-used-icons; I'll send it as a separate PR since the fix touches both scripts.

Happy to iterate on naming, the config location, or to add documentation to the website if the approach suits you.

… CSS

Opt-in script, modeled after only-include-used-icons, that rebuilds
dsfr.css and dsfr.min.css in node_modules (and public/dsfr when
applicable) with only the CSS of the DSFR components actually used by
the project, plus the core and scheme which are always included.

Usage is detected from @codegouvfr/react-dsfr/<Component> imports and
from raw fr-* class names found in the sources. Components can also be
forced via "react-dsfr"."additionalComponents" in package.json.
Any unknown component import falls back to including every component.

The stylesheets are rebuilt from the granular files shipped in dsfr/
(core, scheme, component/*, print variants) preserving the upstream
cascade order, rewriting relative asset urls and reapplying the Mui
compat patch, so no individual CSS rule is ever dropped or rewritten.

See codegouvfr#304
Covers import detection, module to DSFR components resolution, raw
class name detection and stylesheet generation (cascade order, url
rewriting, charset stripping, Mui compat patch, determinism).
A direct import of an unrecognized dsfr/component/<x> stylesheet, and
any unknown lowercase-starting react-dsfr module, returned [] instead
of undefined. This silently skipped the "include every component"
fail-safe and its warning for modules this script does not know
about, instead of only affecting genuinely non-component modules.
Copilot AI lite review requested due to automatic review settings August 14, 2026 15:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new opt-in CLI script (only-include-used-components) to rebuild DSFR CSS bundles by concatenating only the granular component stylesheets that correspond to components detected as used in the target codebase, reducing unused CSS while keeping DSFR JS runtime class toggles safe.

Changes:

  • Add src/bin/only-include-used-components.ts plus CLI wiring via react-dsfr and a dedicated bin entry.
  • Implement component usage detection (react-dsfr imports, raw fr-* class prefixes, and package.json escape hatch) and rebuild dsfr.css / dsfr.min.css (and SPA public patch + hash busting).
  • Add unit tests covering module resolution, detection, URL rewriting, MUI core patching, and CSS generation behavior.

Reviewed changes

Copilot reviewed 5 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
test/runtime/scripts/onlyIncludeUsedComponents/resolveModuleIdToDsfrComponents.test.ts Adds unit tests for moduleId→DSFR component mapping and fail-safe behavior.
test/runtime/scripts/onlyIncludeUsedComponents/getReactDsfrImportedModuleIds.test.ts Adds unit tests for detecting react-dsfr module IDs from source text.
test/runtime/scripts/onlyIncludeUsedComponents/generateDsfrCssCode.test.ts Adds unit tests for CSS reconstruction (order, inclusion/exclusion, URL rewriting, MUI patching).
test/runtime/scripts/onlyIncludeUsedComponents/detectDsfrComponentsFromClassNames.test.ts Adds unit tests for detecting DSFR components via raw fr-* class usage.
src/bin/react-dsfr.ts Wires the new command into the react-dsfr CLI dispatcher.
src/bin/only-include-used-components.ts Implements the new trimming/rebuild script and supporting helpers/constants.
package.json Exposes only-include-used-components as a published bin entry.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@kevbarns kevbarns changed the title Add only-include-used-components: opt-in trimming of unused DSFR component CSS feat(core): Add only-include-used-components: opt-in trimming of unused DSFR component CSS Aug 14, 2026

@lsagetlethias lsagetlethias left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, and thanks for the very detailed description. The approach is the right one: rebuilding from the granular stylesheets instead of purging rules is what makes this safe against the classes the DSFR JS toggles at runtime, and it is a much better answer to #304 than a PurgeCSS pass. The CLI ergonomics matching only-include-used-icons is the right call too.

I checked the core claims independently against @gouvfr/dsfr 1.14.2 rather than taking them on trust, and they hold:

  • The reconstruction is faithful. Rebuilding core + scheme + component/* and diffing rule by rule against dsfr.main.css + dsfr.print.css (the correct target, since scripts/build/build.ts:58 already overwrites dsfr/dsfr.css with that concatenation) gives 4031 rules vs 4030, the only delta being the dark-mode block split in two. Exactly what you documented.
  • The Mui patch is equivalent to the real one. button:not(:disabled):hover|active occurs twice, both in core, both inside (hover: hover) and (pointer: fine). The negative lookahead also makes it safely re-runnable.
  • URL rewriting is correct on the real url() set, and the static tables cover all 66 current entry points, so nothing falls through to the fail-safe today.
  • The numbers reproduce: 581 kB -> 282 kB, 76 kB -> 36 kB gzip on a 5-component app, idempotent on the second run, fail-safe firing correctly on an unknown module.

Two things I would want fixed before merge, left as inline comments: the assets referenced by the regenerated public/dsfr/dsfr.min.css are never copied (404s in production, with a repro), and the fail-safe is currently far too easy to trigger, which silently reverts to the full bundle.

The rest is polish and could land here or as a follow-up:

  1. transcription is at the wrong index in DSFR_COMPONENTS_CASCADE_ORDER (line 83). Upstream section banners in dsfr.main.css read ... TABLE TRANSCRIPTION HEADER, so it belongs between line 98 and line 99, not right after content. I could not exhibit any rendering difference (see point 2), but the array's whole purpose is fidelity. A test asserting it equals the section order extracted from the installed dsfr.main.css would stop it drifting on the next DSFR bump, and would be cheap given the banners are machine-readable.

  2. "The upstream cascade order is preserved" is not quite accurate. The upstream bundle groups rules by media context globally (all unmediated rules from every component, then all @media (min-width: 36em), etc.), while the concatenation groups them by component, which displaces about 29% of the rules. I looked for actual damage and found none: no selector is declared with divergent values in two different granular files, so there is no equal-specificity conflict whose winner can flip. Worth rewording in the description rather than changing the code, since the empirical validation you ran covered the rule set and not the order.

  3. shared (line 242) and link (line 240) are in NON_COMPONENT_MODULE_IDS but do render DSFR markup. src/shared/Fieldset.tsx emits fr-fieldset, fr-fieldset__legend, fr-label, fr-hint-text (form) plus fr-radio-rich, fr-radio-rich__img (radio), and src/link.tsx:40,69 renders fr-link. Both resolve to [], which suppresses the fail-safe instead of triggering it. Exposure is low (undocumented internal subpaths, and the sanctioned wrappers are mapped correctly), but it is the same class of hole your last commit set out to close. Mapping shared to ["form", "radio", "checkbox"] and link to ["link"] would close it.

  4. additionalComponents entries mapping to [] report the opposite of what happens (lines 881-900). "Chart" (line 124) is the only one today: the run prints Including Chart (from package.json additionalComponents) and adds nothing. Someone reaching for that escape hatch is doing so precisely because a component is unstyled, so a misleading confirmation is the worst possible feedback. Worth logging explicitly that the entry maps to no DSFR stylesheet (the Chart CSS comes from @gouvfr/dsfr-chart).

  5. fs.readdirSync on dsfr/component (line 790) has no guard and dumps a raw ENOENT ... scandir stack. The nominal case is fine since the published package ships the folder, but a workspace-linked or pruned node_modules gets an unreadable error. An existsSync + assert(false, "...") in the same register as line 485 would be consistent with the rest of the script.

  6. Two pre-existing behaviours this script inherits verbatim from only-include-used-icons, so not regressions of this PR, but worth a thought since it is proposed as a prebuild step for everyone:

    • assert(htmlFilePath !== undefined) (line 769) throws a message-less AssertionError as soon as public/dsfr exists without an index.html. That is every Next.js project that has run copy-static-assets. I reproduced it, and reproduced the identical crash with only-include-used-icons. Making htmlFilePath optional in spaParams and skipping addHashQueryParameterInIndexHtml would let those projects benefit from the trim instead of crashing.
    • The hasChanged early return (line 986) also skips the index.html hash rewrite, so a stale or hand-reverted hash can never be repaired while the CSS itself is unchanged. The rewrite is already idempotent, so it could simply live outside the guard.
  7. Documentation. src/bin/README.md is a single line and nothing there covers this script; additionalComponents is only described in a source docblock, where a typo in the key silently disables the whole thing. Given the fail-safe makes misconfiguration invisible, a short section on the website (or at least in the README) explaining the ordering constraint with copy-static-assets would go a long way.

Happy to re-review once the two inline points are addressed.

Lilian & Claude 🤖

Comment thread src/bin/only-include-used-components.ts
Comment thread src/bin/only-include-used-components.ts Outdated
Copy the assets referenced by the regenerated public/dsfr/dsfr.min.css.
copy-dsfr-to-public builds its keep list from the url() of the dsfr.min.css it
finds in node_modules, then early returns as long as public/dsfr/version.txt
matches the @gouvfr/dsfr version. Once it had run against an already trimmed
stylesheet, public/dsfr was frozen on that asset subset and growing the component
set later could never bring the missing files back (blank burger, close, search
and alert icons in production). The copy runs outside the `hasChanged` guard so a
purged public/dsfr is repairable even when the CSS itself did not change.

Stop triggering the include-everything fail-safe on non imports. Stylesheets are
no longer scanned (class name detection is substring based, so a single compiled
bundle in out/ or in a dependency marked every component as used) and module ids
are now read from actual import specifiers only, instead of any textual
occurrence: a link to https://www.npmjs.com/package/@codegouvfr/react-dsfr/v/1.32.5
in an .mdx used to resolve to the module "v" and silently ship the full bundle.
Add --strict to exit 1 instead of falling back, for CI where the warning goes
unnoticed and the run still looks like a success.

Move `transcription` between `table` and `header` in DSFR_COMPONENTS_CASCADE_ORDER,
and assert the whole array against the section order extracted from the
`sources` of the installed dsfr.main.css.map so it can't drift on a DSFR bump.

Map `link` and `shared` to the components they render (fr-link for the Link
fallback, fr-fieldset/fr-label/fr-hint-text/fr-radio-rich for Fieldset) instead of
resolving them to no component at all, which suppressed the fail-safe.

Report `additionalComponents` entries that map to no DSFR stylesheet for what they
are instead of confirming an inclusion that does not happen, warn on a typo in the
`additionalComponents` key, and give the missing dsfr/component directory a proper
message instead of a raw ENOENT.

Document the three bin scripts in src/bin/README.md, including the ordering
constraint with copy-static-assets.
@kevbarns

kevbarns commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

Thank you @lsagetlethias for this review — the independent verification of the reconstruction, of the Mui patch and of the numbers is far more than I expected, and both inline points were real. All addressed in 6d268d0, except point 6 (see below).

Inline 1 — assets. copyUsedDsfrAssetsToStatic() copies the non-data: url() of the regenerated stylesheet from node_modules into public/dsfr, mirroring copyUsedDsfrIconsToStatic(). I chose the targeted copy over deleting version.txt so the script stays self-sufficient. It runs outside the hasChanged early return — inside it, a purged public/dsfr would stay broken as long as the CSS didn't change, the same class of problem as your 6b. Replaying your repro: public/dsfr goes from 12 to 19 svg, 0 of the 38 urls missing on disk.

Inline 2 — fail-safe surface. Took the simpler of your two options: css/scss/sass/less are gone, the list is now identical to only-include-used-icons.ts:390. Module ids are read from actual import specifiers only (from, import, import(), require, @import); your two URLs are covered by a negative test, plus a case where a real import sits next to such a mention. You're also right that a warning nobody reads plus exit 0 is not a remedy: --strict now exits 1 before writing anything. Verified — your .mdx URL and a DSFR bundle dropped in out/assets/index.css both leave the output at 9/45 components.

1 — transcription. Moved, and asserted against the installed DSFR. One correction on the justification: dsfr.main.css has no component section banners, only /*! media sm */. What is machine-readable is dsfr.main.css.map — the first occurrence of each component/<name>/main.scss in its sources. That confirms your placement, and also that the rest of the array is exact, including link, whose sources appear early only as style/tool/* mixins inlined into accordion. radio has no main.scss upstream, so the test falls back to its first stylesheet, which lands it between notice and card as the array already had it. Checked the test isn't vacuous: restoring the old position fails it on that exact entry.

2 — reworded in the description, code unchanged, keeping your finding that the reordering is inert.

3shared["form", "radio", "checkbox"], link["link"], both out of NON_COMPONENT_MODULE_IDS.

4Chart now reports that it maps to no DSFR stylesheet and that its CSS comes from @gouvfr/dsfr-chart. Also added a warning when the react-dsfr entry exists without an additionalComponents key — a typo there disabled the escape hatch with no output at all.

5existsSync + assert(false, ...), same register as the existing one.

6 — agreed on both. They're going into a separate PR, opened right after this one, since the fix has to touch only-include-used-icons too and that is outside this PR's scope. Worth noting the scope is narrower than "every Next.js project that has run copy-static-assets": that command asserts "Can't locate your index.html file." before creating anything, so a Next.js project never gets a public/dsfr through it. What does reproduce the crash is public/dsfr existing while no index.html is findable — a monorepo run with --projectDir, or a project that lost its index.html.

7src/bin/README.md now covers the three bin scripts: usage, prebuild integration, the ordering constraint with copy-static-assets and why it exists, additionalComponents, and --strict. Left the website alone since it lives in another repo — happy to send a page there if you tell me where it fits.


Edit: corrected the last sentence of point 6 — I had claimed only-include-used-icons is unusable in Next.js, which isn't the case.

@lsagetlethias lsagetlethias left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Re-checked everything by running it, not by reading:

  • Assets: replayed the repro, public/dsfr goes 12 -> 19 svg, menu-fill.svg present, 0 of 38 urls missing on disk. Same numbers you report.
  • Fail-safe: the two URLs in an .mdx and a DSFR bundle dropped in out/assets/ both leave the output at 9/45. All eight import forms still resolve (named, default, side-effect, dynamic(() => import()), require, re-export, blocks/, @import). --strict exits 1 and writes nothing.
  • Cascade order: verified with a third method, the median position in the bundle of each stylesheet's own rules. All 45 match, radio included. Your test is not vacuous either, moving transcription back fails it on that entry.
  • No regression in generation: the only delta against upstream is the two Mui-patched selectors (intended) and the split dark block (documented).

Two non-blocking notes, both one-liners, take them or leave them.

The section banners do exist. dsfr.main.css has 44, in the form /* ¯¯¯¯¯¯¯¯¯ *\ + name + \* ˍˍˍˍˍˍˍˍˍ */, which is where my ... TABLE TRANSCRIPTION HEADER came from. Your source map choice is still the better one, but for a reason you did not give: the banners are a defective index. badge, consent, notice and radio have none, and notice is labelled ALERT, so the list has alert twice. The map only misses radio. Worth fixing the claim in the test docblock ("There is no section banner in dsfr.main.css to read it from"), otherwise the next person loses a usable cross-check.

The cascade order test passes green when the source map is missing. I moved dsfr.main.css.map out of the way and it reports success. That is the same shape as what you just fixed with --strict: a warning nobody reads plus exit 0. @gouvfr/dsfr is a direct dependency of this repo, so the map is guaranteed present in CI and there is no legitimate skip case. Failing instead of skipping would keep the guard from silently evaporating on a future DSFR that stops shipping maps.

The section banners do exist in dsfr.main.css, my test docblock claimed otherwise.
They are however a defective index, which is the real reason to prefer the source
map: 44 banners for 45 components, `badge`, `consent`, `notice` and `radio` have
none, and `notice` is labelled ALERT so the list contains `alert` twice. The map
only misses `radio`. Docblock corrected to say that instead.

Fail the cascade order test when dsfr.main.css.map is missing, instead of warning
and reporting success. @gouvfr/dsfr is a direct dependency of this repo so there is
no legitimate skip case, and a green skip would let the guard silently evaporate on
a future DSFR that stops shipping source maps.

Also apply here the fix sent for only-include-used-icons in codegouvfr#506, since this file
does not exist on main yet: `spaParams.htmlFilePath` becomes optional so a
public/dsfr without a findable index.html no longer throws a message-less
AssertionError, and the cache busting rewrite moves out of the `hasChanged` early
return so a stale or hand reverted hash is repairable.
@kevbarns

Copy link
Copy Markdown
Collaborator Author

Thanks for the approval, and for re-running everything rather than reading it — the third method on the cascade order (median rule position) is a better check than mine. Both notes were right, both fixed in 287c641.

The banners. You're right and my claim was wrong — 44 of them, my grep had missed them because the banner body contains *\. And your point about why the map is still the better source is the one worth recording: the banners are a defective index (44 for 45 components, badge/consent/notice/radio have none, notice labelled ALERT so alert appears twice), where the map only misses radio. That is now what the test docblock says, instead of denying the banners exist.

Green skip. Fixed, and you're right that it's the same shape as the thing I had just added --strict for. It now asserts the map is present before reading it. Checked both ways: passes normally, fails with dsfr.main.css.map not found when I move the file away.

While I was in there I also applied the #506 fix to this script, since only-include-used-components.ts doesn't exist on main and couldn't ship in that PR: spaParams.htmlFilePath is optional, and the cache busting rewrite moved out of the hasChanged guard. Verified the 6b scenario end to end — reverting the hash by hand while the CSS is unchanged now gets repaired on the next run, where it previously couldn't.

#506 is open for the only-include-used-icons half.

Next.js has no `public/dsfr` in the documented setup, `next-appdir` and
`next-pagesdir` run the trimming scripts without `copy-dsfr-to-public`, so
`spaParams` is undefined there and this field is never reached. The real
trigger is any project with a `public/dsfr` and no `index.html`.

Same correction as on fix/skip-index-html-when-absent, the comment was copied
from there.
It cannot produce this state: copy-dsfr-to-public.ts:60 asserts "Can't locate
your index.html file." before the mkdirSync at :95, so a project with no
index.html never gets a public/dsfr out of it, Next.js included.
@kevbarns

Copy link
Copy Markdown
Collaborator Author

Two small follow-ups pushed here as a consequence of @lsagetlethias's review of #506, both on the same htmlFilePath comment in only-include-used-components.ts, no behaviour change:

  • 8ac9a56 — the comment claimed htmlFilePath is undefined in Next.js because public/dsfr exists. That is wrong, and it had been copied into this PR from fix(bin): Don't crash only-include-used-icons when public/dsfr exists without an index.html #506. next-appdir and next-pagesdir run the trimming scripts without copy-dsfr-to-public, so there is no public/dsfr there at all, spaParams is undefined, and the field is never reached.
  • a84376f — dropped the replacement's claim that copy-static-assets can produce this state. It cannot: copy-dsfr-to-public.ts:60 asserts "Can't locate your index.html file." before the mkdirSync(dsfrDirPath) at :95, so a project with no index.html never gets a public/dsfr out of it. This is consistent with point 6 of my earlier reply above.

For the record, the two substantive findings of that review do not apply here, checked explicitly:

  • the hasChanged guard already has the right shape in this script — only clearCache sits behind it, the asset copies are ahead of it;
  • ordering is deterministic here, the component set is consumed through availableDsfrComponents.filter(...) rather than through the Set's insertion order, so the generated stylesheet is byte stable across runs. only-include-used-icons was not, and that is fixed in fix(bin): Don't crash only-include-used-icons when public/dsfr exists without an index.html #506.

103 tests, eslint and prettier clean.

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.

CSS splitting

4 participants