Skip to content

Commit 68cc82f

Browse files
committed
perf(react-icons): reuse canonical fonts instead of regenerating
react-icons previously re-ran fantasticon 4x (~48s/variant) to build font families that already exist in the repo-root fonts/ source of truth. Replace that with a copy step. - importer/generateFont.js: make font output byte-deterministic (ttf.ts=0 pins svg2ttf timestamps; sort glob for stable codepoints) - importer/package.json: seed Light/Resizable codepoints from root fonts/ so they stay stable across releases (previously unseeded -> drifted) - react-icons: replace generate:font-* (fantasticon) with copy-base-fonts.js that copies binaries + glyph-named JSON from root fonts/; drop unused concurrently dep - nx: add cacheable deploy:fonts target (importer/project.json) keyed on assets/ via new "fonts" namedInput; react-icons build invalidates on root fonts/ changes - rename nx project @fluentui/system-icons-processing -> system-icons-processing
1 parent 56b3865 commit 68cc82f

8 files changed

Lines changed: 127 additions & 12 deletions

File tree

importer/generateFont.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ async function main() {
7171
await fs.mkdir(stagingFolder, { recursive: true });
7272

7373
/** @type {string[]} */
74-
const svgFiles = await glob(path.resolve(SRC_PATH, `*_${ICON_TYPE === 'Resizable' ? '20_{filled,regular,light}' : ICON_TYPE.toLowerCase()}.svg`));
74+
// Sort glob results so codepoint assignment (and thus the generated font
75+
// bytes) are deterministic regardless of the filesystem's readdir order.
76+
const svgFiles = (await glob(path.resolve(SRC_PATH, `*_${ICON_TYPE === 'Resizable' ? '20_{filled,regular,light}' : ICON_TYPE.toLowerCase()}.svg`))).sort();
7577
const icons = new Set(svgFiles.map(file => path.basename(file).replace(/\.svg$/, '')));
7678

7779
if (icons.size > MAX_PRIVATE_USE_CODEPOINTS) {
@@ -90,7 +92,9 @@ async function main() {
9092
name: `FluentSystemIcons-${ICON_TYPE}`,
9193
fontTypes: [fantasticon.ASSET_TYPES.WOFF2, fantasticon.ASSET_TYPES.WOFF, fantasticon.ASSET_TYPES.TTF],
9294
assetTypes: [fantasticon.ASSET_TYPES.CSS, fantasticon.ASSET_TYPES.HTML, fantasticon.ASSET_TYPES.JSON],
93-
formatOptions: { json: { indent: 2 } },
95+
// `ttf.ts: 0` pins svg2ttf's created/modified timestamps so font binaries
96+
// are byte-deterministic across builds (WOFF/WOFF2 wrap the TTF bytes).
97+
formatOptions: { json: { indent: 2 }, ttf: { ts: 0 } },
9498
codepoints: await getCodepoints(icons),
9599
fontHeight: 500,
96100
normalize: true

importer/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
"deploy:ios": "python3 process_ios_assets.py",
2222
"generate:font-regular": "node generateFont.js --source=dist --dest=dist/fonts --iconType=Regular --codepoints=../fonts/FluentSystemIcons-Regular.json",
2323
"generate:font-filled": "node generateFont.js --source=dist --dest=dist/fonts --iconType=Filled --codepoints=../fonts/FluentSystemIcons-Filled.json",
24-
"generate:font-light": "node generateFont.js --source=dist --dest=dist/fonts --iconType=Light",
25-
"generate:font-resizable": "node generateFont.js --source=dist --dest=dist/fonts --iconType=Resizable",
24+
"generate:font-light": "node generateFont.js --source=dist --dest=dist/fonts --iconType=Light --codepoints=../fonts/FluentSystemIcons-Light.json",
25+
"generate:font-resizable": "node generateFont.js --source=dist --dest=dist/fonts --iconType=Resizable --codepoints=../fonts/FluentSystemIcons-Resizable.json",
2626
"build:fonts": "npm run generate:svg && find ./dist -type f -name '*.svg' -exec svgo --config svgo.config.mjs {} + && mkdir dist/fonts && npm run generate:font-regular && npm run generate:font-filled && npm run generate:font-light && npm run generate:font-resizable && node replace-in-files.js '\\\\' '0x' dist/fonts/*.json",
2727
"deploy:fonts": "npm run build:fonts && cp -a dist/fonts/* ../fonts && npm run clean",
2828
"generate:flutter-icon-lib-class": "node generate_flutter_lib_class.js --source=../fonts/FluentSystemIcons-Regular.json ../fonts/FluentSystemIcons-Filled.json ../fonts/FluentSystemIcons-Light.json --dest=dist/flutter",

importer/project.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "../node_modules/nx/schemas/project-schema.json",
3+
"name": "system-icons-processing",
4+
"projectType": "library",
5+
"targets": {
6+
"deploy:fonts": {
7+
"cache": true,
8+
"inputs": ["fonts"],
9+
"outputs": ["{workspaceRoot}/fonts"]
10+
}
11+
}
12+
}

nx.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99
},
1010
"namedInputs": {
1111
"default": ["{projectRoot}/**/*", "sharedGlobals", "{workspaceRoot}/.github/workflows/pr.yml"],
12+
"fonts": [
13+
"{workspaceRoot}/assets/**/*",
14+
"{workspaceRoot}/importer/generate.js",
15+
"{workspaceRoot}/importer/generateFont.js",
16+
"{workspaceRoot}/importer/replace-in-files.js",
17+
"{workspaceRoot}/importer/svgo.config.mjs",
18+
"{workspaceRoot}/importer/svgo.config.shared.cjs",
19+
"{workspaceRoot}/importer/package.json"
20+
],
1221
"sharedGlobals": []
1322
},
1423
"release": {

packages/react-icons/package.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,7 @@
1717
"convert:svg": "node scripts/convert.js --source=./intermediate --dest=./src --perIconDest=./src/atoms/svg --spriteDest=./src/atoms/svg-sprite --headlessPerIconDest=./src/atoms/headless-svg --headlessSpriteDest=./src/atoms/headless-svg-sprite --rtl=./intermediate/rtl.json --metadata=./tmp/metadata-svg.json",
1818
"convert:fonts": "node scripts/convert-font.js --source=./src/utils/fonts --dest=./src/fonts --perIconDest=./src/atoms/fonts --headlessPerIconDest=./src/atoms/headless-fonts --codepointDest=./src/utils/fonts --rtl=./intermediate/rtl.json --metadata=./tmp/metadata-font.json",
1919
"convert:merge-metadata": "node scripts/merge-metadata.js --svgMetadata=./tmp/metadata-svg.json --fontMetadata=./tmp/metadata-font.json --output=./metadata.json",
20-
"generate:font-regular": "node ../../importer/generateFont.js --source=intermediate --dest=src/utils/fonts --iconType=Regular --codepoints=../../fonts/FluentSystemIcons-Regular.json",
21-
"generate:font-filled": "node ../../importer/generateFont.js --source=intermediate --dest=src/utils/fonts --iconType=Filled --codepoints=../../fonts/FluentSystemIcons-Filled.json",
22-
"generate:font-light": "node ../../importer/generateFont.js --source=intermediate --dest=src/utils/fonts --iconType=Light",
23-
"generate:font-resizable": "node ../../importer/generateFont.js --source=intermediate --dest=src/utils/fonts --iconType=Resizable",
24-
"generate:font": "concurrently 'npm run generate:font-regular' 'npm run generate:font-filled' 'npm run generate:font-light' 'npm run generate:font-resizable'",
20+
"generate:font": "node scripts/copy-base-fonts.js --source=../../fonts --dest=src/utils/fonts",
2521
"generate:rtl": "node ../../importer/rtlMetadata.js --source=../../assets --dest=./intermediate/rtl.json",
2622
"optimize": "svgo --config svgo.config.js --folder=./intermediate --precision=2 --quiet",
2723
"unfill": "node scripts/unfill.js --source=./intermediate",
@@ -42,7 +38,6 @@
4238
"@griffel/eslint-plugin": "^2.0.2",
4339
"@types/react": "^17.0.2",
4440
"@types/yargs": "^15.0.0",
45-
"concurrently": "^9.2.0",
4641
"eslint": "8.57.1",
4742
"eslint-plugin-react-hooks": "^5.2.0",
4843
"glob": "^7.2.0",

packages/react-icons/project.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44
"projectType": "library",
55
"sourceRoot": "packages/react-icons/src",
66
"tags": ["platform:web"],
7-
"implicitDependencies": ["@fluentui/system-icons-processing"],
7+
"implicitDependencies": ["system-icons-processing"],
88
"targets": {
99
"build": {
1010
"cache": true,
1111
"inputs": [
1212
"default",
13+
"{workspaceRoot}/fonts/FluentSystemIcons-*.ttf",
14+
"{workspaceRoot}/fonts/FluentSystemIcons-*.woff",
15+
"{workspaceRoot}/fonts/FluentSystemIcons-*.woff2",
16+
"{workspaceRoot}/fonts/FluentSystemIcons-*.json",
1317
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
1418
"!{projectRoot}/tsconfig.spec.json",
1519
"!{projectRoot}/vitest.config.[jt]s",
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// @ts-check
2+
// Copyright (c) Microsoft Corporation.
3+
// Licensed under the MIT license.
4+
5+
// Populates ./src/utils/fonts from the canonical font binaries committed in the
6+
// repo-root `fonts/` folder (the single source of truth, regenerated at release
7+
// time by `importer`'s `deploy:fonts`). Instead of re-running fantasticon four
8+
// times during every react-icons build, we simply copy the already-generated
9+
// font families and their glyph-named codepoint maps.
10+
//
11+
// Copied per family (Regular / Filled / Light / Resizable):
12+
// - {ttf,woff,woff2} -> imported by createFluentFontIcon.styles.ts (@font-face)
13+
// - .json (glyph-named) -> read by convert-font.js, which then overwrites it
14+
// in place with the React-named codepoint map.
15+
//
16+
// The generated `.css` / `.html` fantasticon byproducts are intentionally NOT
17+
// copied: nothing in the react-icons build consumes them.
18+
19+
const fs = require('node:fs');
20+
const path = require('node:path');
21+
const { parseArgs } = require('node:util');
22+
23+
const FONT_TYPES = ['Regular', 'Filled', 'Light', 'Resizable'];
24+
const BINARY_EXTENSIONS = ['ttf', 'woff', 'woff2'];
25+
26+
if (require.main === module) {
27+
try {
28+
main();
29+
} catch (err) {
30+
console.error('[copy-base-fonts] failed:', err);
31+
process.exit(1);
32+
}
33+
}
34+
35+
function main() {
36+
const {
37+
values: { source, dest, help },
38+
} = parseArgs({
39+
options: {
40+
source: { type: 'string' },
41+
dest: { type: 'string' },
42+
help: { type: 'boolean', short: 'h' },
43+
},
44+
});
45+
46+
if (help) {
47+
printUsage();
48+
return;
49+
}
50+
51+
if (!source) throw new Error('Font source folder not specified by --source');
52+
if (!dest) throw new Error('Output destination folder not specified by --dest');
53+
54+
const sourceDir = path.resolve(source);
55+
const destDir = path.resolve(dest);
56+
57+
if (!fs.existsSync(sourceDir)) {
58+
throw new Error(`Font source folder not found: ${sourceDir}. The repo-root fonts/ folder is the source of truth.`);
59+
}
60+
61+
fs.mkdirSync(destDir, { recursive: true });
62+
63+
let copied = 0;
64+
for (const type of FONT_TYPES) {
65+
const baseName = `FluentSystemIcons-${type}`;
66+
for (const ext of [...BINARY_EXTENSIONS, 'json']) {
67+
const fileName = `${baseName}.${ext}`;
68+
const srcPath = path.join(sourceDir, fileName);
69+
if (!fs.existsSync(srcPath)) {
70+
throw new Error(`Expected font file missing in source: ${srcPath}`);
71+
}
72+
fs.copyFileSync(srcPath, path.join(destDir, fileName));
73+
copied++;
74+
}
75+
}
76+
77+
console.log(`[copy-base-fonts] Copied ${copied} font file(s) from ${sourceDir} -> ${destDir}`);
78+
}
79+
80+
function printUsage() {
81+
console.log(
82+
`Usage: node copy-base-fonts.js --source <dir> --dest <dir>\n\n` +
83+
`Copies the canonical font binaries and glyph-named codepoint maps from the\n` +
84+
`repo-root fonts/ folder into the react-icons build, avoiding a redundant\n` +
85+
`fantasticon run.\n\n` +
86+
`Options:\n` +
87+
` --source <dir> Source folder containing FluentSystemIcons-*.{ttf,woff,woff2,json} (required)\n` +
88+
` --dest <dir> Destination folder to copy the font files into (required)\n` +
89+
` -h, --help Show this help message`,
90+
);
91+
}

packages/react-native-icons/project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
"projectType": "library",
55
"sourceRoot": "packages/react-native-icons/src",
66
"tags": ["platform:react-native"],
7-
"implicitDependencies": ["@fluentui/system-icons-processing"]
7+
"implicitDependencies": ["system-icons-processing"]
88
}

0 commit comments

Comments
 (0)