Skip to content

Commit 6d0d615

Browse files
committed
fix(ui): ship font licenses with font assets
1 parent 5abf04f commit 6d0d615

4 files changed

Lines changed: 114 additions & 4 deletions

File tree

apps/docs/src/content/docs/ui-library/overview.mdx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,32 @@ The package has six style surfaces:
4242
component, typography or font rules.
4343

4444
The stylesheets reference `./assets/*.woff2`, so preserve the package's `dist`
45-
layout when copying or serving them.
45+
layout when copying or serving them. The font assets' SIL Open Font License 1.1
46+
texts ship beside them as `assets/OFL-poppins.txt` and `assets/OFL-inter.txt`.
4647

4748
Only Poppins latin 400 and 600 are inline. Other weights, Inter, and non-ASCII
4849
glyphs use `font-display: swap` assets, so they can briefly appear in the
49-
fallback font while loading. Preload the relevant `.woff2` files when that
50-
flash of unstyled text (FOUT) is unacceptable:
50+
fallback font while loading.
51+
52+
### Shipped font faces
53+
54+
| Family | Weight | Subset | File name | Delivery |
55+
| ------- | ------ | --------- | ------------------------------------ | -------- |
56+
| Poppins | 300 | latin | `poppins-latin-300-normal.woff2` | Asset |
57+
| Poppins | 400 | latin | `poppins-latin-400-normal.woff2` | Inline |
58+
| Poppins | 500 | latin | `poppins-latin-500-normal.woff2` | Asset |
59+
| Poppins | 600 | latin | `poppins-latin-600-normal.woff2` | Inline |
60+
| Poppins | 700 | latin | `poppins-latin-700-normal.woff2` | Asset |
61+
| Poppins | 300 | latin-ext | `poppins-latin-ext-300-normal.woff2` | Asset |
62+
| Poppins | 400 | latin-ext | `poppins-latin-ext-400-normal.woff2` | Asset |
63+
| Poppins | 500 | latin-ext | `poppins-latin-ext-500-normal.woff2` | Asset |
64+
| Poppins | 600 | latin-ext | `poppins-latin-ext-600-normal.woff2` | Asset |
65+
| Poppins | 700 | latin-ext | `poppins-latin-ext-700-normal.woff2` | Asset |
66+
| Inter | 400 | latin | `inter-latin-400-normal.woff2` | Asset |
67+
| Inter | 400 | latin-ext | `inter-latin-ext-400-normal.woff2` | Asset |
68+
69+
Preload the relevant `.woff2` files when that flash of unstyled text (FOUT) is
70+
unacceptable:
5171

5272
```html
5373
<link rel="preload" href="./assets/poppins-latin-ext-400-normal.woff2" as="font" type="font/woff2" crossorigin />

packages/ui/combine-css-bundle.mts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,14 @@ const FONT_FACES: FontFaceDefinition[] = [
103103
},
104104
];
105105

106+
const FONT_LICENSE_FILES: Record<FontFaceDefinition['packageName'], string> = {
107+
'@fontsource/inter': 'OFL-inter.txt',
108+
'@fontsource/poppins': 'OFL-poppins.txt',
109+
};
110+
106111
const require = createRequire(import.meta.url);
107112

108-
function emitFontAssets(distributionDirectory: string): string {
113+
export function emitFontAssets(distributionDirectory: string): string {
109114
const assetsDirectory = path.resolve(distributionDirectory, 'assets');
110115
fs.mkdirSync(assetsDirectory, { recursive: true });
111116

@@ -116,6 +121,10 @@ function emitFontAssets(distributionDirectory: string): string {
116121
if (!packageDirectory) {
117122
packageDirectory = path.dirname(require.resolve(`${face.packageName}/package.json`));
118123
packageDirectories.set(face.packageName, packageDirectory);
124+
fs.copyFileSync(
125+
path.resolve(packageDirectory, 'LICENSE'),
126+
path.resolve(assetsDirectory, FONT_LICENSE_FILES[face.packageName]),
127+
);
119128
}
120129

121130
let packageUnicodeRanges = unicodeRanges.get(face.packageName);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Ship common font faces inline and the rest as assets
2+
3+
### Title: Ship common font faces inline and the rest as assets
4+
5+
### Proposed by: Jan Librowski
6+
7+
### Date: 31.08.2026
8+
9+
## Context
10+
11+
Vite library mode inlined all twelve Poppins and Inter font faces as base64.
12+
The built stylesheet consequently carried approximately 382 KB of fonts:
13+
`index.css` was 509 KB, and the SDK stylesheet that bundles it was 591 KB.
14+
Most consumers need only the Poppins 400 and 600 latin faces declared by the
15+
typography classes, while the other weights, Inter, and extended latin subsets
16+
can load on demand.
17+
18+
## Decision
19+
20+
Generate the twelve `@font-face` rules after Vite finishes. Keep Poppins 400
21+
and 600 latin inline, and copy the other ten `.woff2` files into `dist/assets`.
22+
Apply fontsource's subset-specific unicode ranges and `font-display: swap` so a
23+
browser requests only the faces needed by the document. Ship the Poppins and
24+
Inter SIL Open Font License 1.1 texts beside the font assets.
25+
26+
This reduces `index.css` from 509 KB to 150 KB and the SDK stylesheet from 591
27+
KB to 230 KB while preserving existing imports and immediate rendering for the
28+
two common faces.
29+
30+
## Alternative Options Considered
31+
32+
- **Keep everything inline.** Rejected because every consumer would continue
33+
downloading approximately 382 KB of font data before using any face.
34+
- **Ship everything as assets.** Rejected because even the common Poppins 400
35+
and 600 latin faces would require additional requests before normal UI text
36+
renders.
37+
- **Patch the Vite configuration.** Rejected because Vite library mode ignores
38+
the normal asset inline limit, so configuration alone cannot produce the
39+
required mix of inline and emitted faces.
40+
- **Use a CDN.** Rejected because it adds an external runtime dependency,
41+
changes Content Security Policy requirements, and prevents the package from
42+
remaining self-contained.
43+
44+
## Consequences
45+
46+
- Consumers that define `font-src` in Content Security Policy must allow
47+
`'self'` or the package-serving origin instead of relying only on `data:`.
48+
- Consumers that copy stylesheets must preserve the relative `dist/assets`
49+
layout.
50+
- A future refactor that simplifies the pipeline back to fontsource CSS imports
51+
can silently reintroduce approximately 382 KB of inline font data.
52+
- The build must keep the font asset references and shipped license files in
53+
sync with `FONT_FACES`.
54+
55+
## Status
56+
57+
accepted

packages/ui/font-license.spec.mts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import fs from 'node:fs';
2+
import os from 'node:os';
3+
import path from 'node:path';
4+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
5+
6+
import { emitFontAssets } from './combine-css-bundle.mts';
7+
8+
describe('font license assets', () => {
9+
const distributionDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'wb-ui-font-licenses-'));
10+
11+
beforeAll(() => {
12+
emitFontAssets(distributionDirectory);
13+
});
14+
15+
afterAll(() => {
16+
fs.rmSync(distributionDirectory, { recursive: true, force: true });
17+
});
18+
19+
it.each(['OFL-poppins.txt', 'OFL-inter.txt'])('emits %s with the font assets', (fileName) => {
20+
const license = fs.readFileSync(path.resolve(distributionDirectory, 'assets', fileName), 'utf8');
21+
22+
expect(license).toContain('SIL OPEN FONT LICENSE Version 1.1');
23+
});
24+
});

0 commit comments

Comments
 (0)