Skip to content

Commit 659a084

Browse files
authored
feat(core): Add only-include-used-components: opt-in trimming of unused DSFR component CSS (#505)
* Add only-include-used-components script to trim unused DSFR component 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.
1 parent 0deb6e6 commit 659a084

9 files changed

Lines changed: 1771 additions & 1 deletion

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
"bin": {
3131
"react-dsfr": "dist/bin/react-dsfr.js",
3232
"copy-dsfr-to-public": "dist/bin/copy-dsfr-to-public.js",
33-
"only-include-used-icons": "dist/bin/only-include-used-icons.js"
33+
"only-include-used-icons": "dist/bin/only-include-used-icons.js",
34+
"only-include-used-components": "dist/bin/only-include-used-components.js"
3435
},
3536
"lint-staged": {
3637
"*.{ts,tsx}": [

src/bin/README.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,108 @@
11
Here are the scripts exposed as utility to the user of `react-dsfr`
2+
3+
| Command | Standalone bin | What it does |
4+
| --------------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------- |
5+
| `npx react-dsfr copy-static-assets` | `copy-dsfr-to-public` | Copies the DSFR assets into `public/dsfr` (SPA setups: Vite, CRA). |
6+
| `npx react-dsfr update-icons` | `only-include-used-icons` | Rebuilds `dsfr/utility/icons/icons.min.css` with only the icons you use. |
7+
| `npx react-dsfr only-include-used-components` | `only-include-used-components` | Rebuilds `dsfr/dsfr.css` and `dsfr/dsfr.min.css` with only the components you use. |
8+
9+
Every script accepts `--projectDir <path>` to point at the react project (monorepos),
10+
defaulting to the current working directory. `update-icons` and
11+
`only-include-used-components` also accept `--silent` to disable the `console.log`
12+
(warnings are never silenced).
13+
14+
# `only-include-used-components`
15+
16+
Opt-in. `dsfr.min.css` weighs ~600 kB raw / ~76 kB gzip and is loaded render-blocking,
17+
while most apps use a small subset of the DSFR components.
18+
19+
This script rebuilds `dsfr.css` and `dsfr.min.css` in `node_modules` (and
20+
`public/dsfr/dsfr.min.css` in SPA setups) by concatenating the granular stylesheets
21+
already shipped in the package (`dsfr/core/*`, `dsfr/scheme/*`, `dsfr/component/<name>/*`).
22+
**Whole components are included or excluded, never individual rules**, so everything the
23+
DSFR JavaScript toggles at runtime (`data-fr-js-*`, `fr-collapse--expanded`, ...) keeps
24+
working — unlike a PurgeCSS style pass.
25+
26+
## Usage
27+
28+
```bash
29+
npx react-dsfr only-include-used-components
30+
```
31+
32+
Typically as a `prebuild`/`predev` step:
33+
34+
```jsonc
35+
"scripts": {
36+
"predev": "react-dsfr update-icons && react-dsfr only-include-used-components",
37+
"prebuild": "react-dsfr update-icons && react-dsfr only-include-used-components"
38+
}
39+
```
40+
41+
## Ordering with `copy-static-assets`
42+
43+
In SPA setups (Vite, CRA), run `copy-static-assets` **before**
44+
`only-include-used-components`, not after:
45+
46+
```jsonc
47+
"prebuild": "react-dsfr copy-static-assets && react-dsfr update-icons && react-dsfr only-include-used-components"
48+
```
49+
50+
`copy-static-assets` builds its keep list from the `url()` of the `dsfr.min.css` it finds
51+
in `node_modules`, then early returns on every later run as long as
52+
`public/dsfr/version.txt` matches the `@gouvfr/dsfr` version. Running it against an
53+
already trimmed stylesheet freezes `public/dsfr` on that asset subset.
54+
`only-include-used-components` copies the assets its own output references, so a component
55+
added later still gets its icons — but keeping the order above avoids relying on it.
56+
57+
## Detection of used components
58+
59+
1. **Imports** of `@codegouvfr/react-dsfr/<Module>` in your sources
60+
(`.ts`, `.tsx`, `.js`, `.jsx`, `.mdx`, `.html`, `.svelte`, `.vue`), resolved through a
61+
static table that includes transitive dependencies (a `Header` renders a navigation, a
62+
search bar and a modal).
63+
2. **Raw class names**, e.g. `fr.cx("fr-table")` or a plain `class="fr-table"`, for when
64+
you use DSFR classes without the React component.
65+
66+
Stylesheets (`.css`, `.scss`, ...) are **not** scanned: class name detection is substring
67+
based, so a single compiled bundle would mark every component as used. Use
68+
`additionalComponents` below for the components you only reference from a stylesheet.
69+
70+
## `additionalComponents`, the escape hatch
71+
72+
For anything the detection cannot see (class names built dynamically, CMS content,
73+
components only referenced from a `@import`ed stylesheet), in your **`package.json`**:
74+
75+
```jsonc
76+
{
77+
"react-dsfr": {
78+
"additionalComponents": ["table", "Range"]
79+
}
80+
}
81+
```
82+
83+
Values are DSFR CSS component names (the `dsfr/component/<name>` directories) or
84+
react-dsfr component names. An unknown value is a hard warning, not a silent no-op.
85+
86+
## Fail-safe and `--strict`
87+
88+
If anything can't be resolved — typically a react-dsfr module added in a newer release that
89+
this script does not know about — the script **warns and includes every component**. The
90+
output is then equivalent to the original bundle: never a broken page, but no trimming
91+
either, and the run still exits `0`.
92+
93+
Because nobody reads warnings in CI, add `--strict` there to turn that fallback into a
94+
failure:
95+
96+
```bash
97+
npx react-dsfr only-include-used-components --strict
98+
```
99+
100+
Please [report](https://github.com/codegouvfr/react-dsfr/issues) any module that triggers
101+
the fail-safe, the static tables need to be updated.
102+
103+
## Known limitations
104+
105+
- Detection is textual: a dynamically composed import path or class name is not seen.
106+
That is what `additionalComponents` is for.
107+
- `utility/colors` and `utility/icons` are not part of `dsfr.css` upstream and are left
108+
untouched (icons are handled by `update-icons`).

0 commit comments

Comments
 (0)