Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 75 additions & 52 deletions packages/react-icons/build-verify.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';

import { FONT_FAMILIES } from './scripts/font-families';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

/**
Expand Down Expand Up @@ -289,40 +291,36 @@ describe('Build Verification', () => {
const processed = 'utils/fonts/createFluentFontIcon.styles.js';
const unprocessed = 'utils/fonts/createFluentFontIcon.styles.raw.js';
expect(fs.readFileSync(path.join(root, processed), 'utf8')).toMatchInlineSnapshot(`
"import _asset10 from "./FluentSystemIcons-Resizable.ttf";
import _asset1 from "./FluentSystemIcons-Resizable.woff";
import _asset0 from "./FluentSystemIcons-Resizable.woff2";
import _asset9 from "./FluentSystemIcons-Light.ttf";
import _asset8 from "./FluentSystemIcons-Light.woff";
import _asset7 from "./FluentSystemIcons-Light.woff2";
"import _asset10 from "./FluentSystemIcons-Light.ttf";
import _asset1 from "./FluentSystemIcons-Light.woff";
import _asset0 from "./FluentSystemIcons-Light.woff2";
import _asset9 from "./FluentSystemIcons-Resizable.ttf";
import _asset8 from "./FluentSystemIcons-Resizable.woff";
import _asset7 from "./FluentSystemIcons-Resizable.woff2";
import _asset6 from "./FluentSystemIcons-Regular.ttf";
import _asset5 from "./FluentSystemIcons-Regular.woff";
import _asset4 from "./FluentSystemIcons-Regular.woff2";
import _asset3 from "./FluentSystemIcons-Filled.ttf";
import _asset2 from "./FluentSystemIcons-Filled.woff";
import _asset from "./FluentSystemIcons-Filled.woff2";
import { __styles, __staticStyles } from '@griffel/react';
import fontFilledTtf from './FluentSystemIcons-Filled.ttf';
import fontFilledWoff from './FluentSystemIcons-Filled.woff';
// #region @generated font-faces
import fontFilledWoff2 from './FluentSystemIcons-Filled.woff2';
import fontRegularTtf from './FluentSystemIcons-Regular.ttf';
import fontRegularWoff from './FluentSystemIcons-Regular.woff';
import fontFilledWoff from './FluentSystemIcons-Filled.woff';
import fontFilledTtf from './FluentSystemIcons-Filled.ttf';
import fontRegularWoff2 from './FluentSystemIcons-Regular.woff2';
import fontLightTtf from './FluentSystemIcons-Light.ttf';
import fontLightWoff from './FluentSystemIcons-Light.woff';
import fontRegularWoff from './FluentSystemIcons-Regular.woff';
import fontRegularTtf from './FluentSystemIcons-Regular.ttf';
import fontResizableWoff2 from './FluentSystemIcons-Resizable.woff2';
import fontResizableWoff from './FluentSystemIcons-Resizable.woff';
import fontResizableTtf from './FluentSystemIcons-Resizable.ttf';
import fontLightWoff2 from './FluentSystemIcons-Light.woff2';
import fontOneSizeTtf from './FluentSystemIcons-Resizable.ttf';
import fontOneSizeWoff from './FluentSystemIcons-Resizable.woff';
import fontOneSizeWoff2 from './FluentSystemIcons-Resizable.woff2';
const FONT_FAMILY_MAP = {
[0 /* Filled */]: 'FluentSystemIconsFilled',
[1 /* Regular */]: 'FluentSystemIconsRegular',
[2 /* Resizable */]: 'FluentSystemIcons',
[3 /* Light */]: 'FluentSystemIconsLight'
};
import fontLightWoff from './FluentSystemIcons-Light.woff';
import fontLightTtf from './FluentSystemIcons-Light.ttf';
export const useStaticStyles = __staticStyles({
d: [\`@font-face{font-family:FluentSystemIconsFilled;font-display:"block";src:url(\${_asset}) format("woff2"),url(\${_asset2}) format("woff"),url(\${_asset3}) format("truetype");}\`, \`@font-face{font-family:FluentSystemIconsRegular;font-display:"block";src:url(\${_asset4}) format("woff2"),url(\${_asset5}) format("woff"),url(\${_asset6}) format("truetype");}\`, \`@font-face{font-family:FluentSystemIconsLight;font-display:"block";src:url(\${_asset7}) format("woff2"),url(\${_asset8}) format("woff"),url(\${_asset9}) format("truetype");}\`, \`@font-face{font-family:FluentSystemIcons;font-display:"block";src:url(\${_asset0}) format("woff2"),url(\${_asset1}) format("woff"),url(\${_asset10}) format("truetype");}\`]
d: [\`@font-face{font-family:FluentSystemIconsFilled;font-display:"block";src:url(\${_asset}) format("woff2"),url(\${_asset2}) format("woff"),url(\${_asset3}) format("truetype");}\`, \`@font-face{font-family:FluentSystemIconsRegular;font-display:"block";src:url(\${_asset4}) format("woff2"),url(\${_asset5}) format("woff"),url(\${_asset6}) format("truetype");}\`, \`@font-face{font-family:FluentSystemIcons;font-display:"block";src:url(\${_asset7}) format("woff2"),url(\${_asset8}) format("woff"),url(\${_asset9}) format("truetype");}\`, \`@font-face{font-family:FluentSystemIconsLight;font-display:"block";src:url(\${_asset0}) format("woff2"),url(\${_asset1}) format("woff"),url(\${_asset10}) format("truetype");}\`]
});
// #endregion @generated font-faces
export const useRootStyles = __styles({
"0": {
Bahqtrf: "f9dzkbp"
Expand Down Expand Up @@ -355,56 +353,54 @@ describe('Build Verification', () => {
`);
expect(fs.readFileSync(path.join(root, unprocessed), 'utf8')).toMatchInlineSnapshot(`
"import { makeStyles, makeStaticStyles } from '@griffel/react';
import fontFilledTtf from './FluentSystemIcons-Filled.ttf';
import fontFilledWoff from './FluentSystemIcons-Filled.woff';
// #region @generated font-faces
import fontFilledWoff2 from './FluentSystemIcons-Filled.woff2';
import fontRegularTtf from './FluentSystemIcons-Regular.ttf';
import fontRegularWoff from './FluentSystemIcons-Regular.woff';
import fontFilledWoff from './FluentSystemIcons-Filled.woff';
import fontFilledTtf from './FluentSystemIcons-Filled.ttf';
import fontRegularWoff2 from './FluentSystemIcons-Regular.woff2';
import fontLightTtf from './FluentSystemIcons-Light.ttf';
import fontLightWoff from './FluentSystemIcons-Light.woff';
import fontRegularWoff from './FluentSystemIcons-Regular.woff';
import fontRegularTtf from './FluentSystemIcons-Regular.ttf';
import fontResizableWoff2 from './FluentSystemIcons-Resizable.woff2';
import fontResizableWoff from './FluentSystemIcons-Resizable.woff';
import fontResizableTtf from './FluentSystemIcons-Resizable.ttf';
import fontLightWoff2 from './FluentSystemIcons-Light.woff2';
import fontOneSizeTtf from './FluentSystemIcons-Resizable.ttf';
import fontOneSizeWoff from './FluentSystemIcons-Resizable.woff';
import fontOneSizeWoff2 from './FluentSystemIcons-Resizable.woff2';
const FONT_FAMILY_MAP = {
[0 /* Filled */]: 'FluentSystemIconsFilled',
[1 /* Regular */]: 'FluentSystemIconsRegular',
[2 /* Resizable */]: 'FluentSystemIcons',
[3 /* Light */]: 'FluentSystemIconsLight',
};
import fontLightWoff from './FluentSystemIcons-Light.woff';
import fontLightTtf from './FluentSystemIcons-Light.ttf';
export const useStaticStyles = makeStaticStyles(\`
@font-face {
font-family: \${FONT_FAMILY_MAP[0 /* Filled */]};
font-family: FluentSystemIconsFilled;
font-display: "block";
src: url(\${JSON.stringify(fontFilledWoff2)}) format("woff2"),
src:
url(\${JSON.stringify(fontFilledWoff2)}) format("woff2"),
url(\${JSON.stringify(fontFilledWoff)}) format("woff"),
url(\${JSON.stringify(fontFilledTtf)}) format("truetype");
}
@font-face {
font-family: \${FONT_FAMILY_MAP[1 /* Regular */]};
font-family: FluentSystemIconsRegular;
font-display: "block";
src: url(\${JSON.stringify(fontRegularWoff2)}) format("woff2"),
src:
url(\${JSON.stringify(fontRegularWoff2)}) format("woff2"),
url(\${JSON.stringify(fontRegularWoff)}) format("woff"),
url(\${JSON.stringify(fontRegularTtf)}) format("truetype");
}

@font-face {
font-family: \${FONT_FAMILY_MAP[3 /* Light */]};
font-family: FluentSystemIcons;
font-display: "block";
src: url(\${JSON.stringify(fontLightWoff2)}) format("woff2"),
url(\${JSON.stringify(fontLightWoff)}) format("woff"),
url(\${JSON.stringify(fontLightTtf)}) format("truetype");
src:
url(\${JSON.stringify(fontResizableWoff2)}) format("woff2"),
url(\${JSON.stringify(fontResizableWoff)}) format("woff"),
url(\${JSON.stringify(fontResizableTtf)}) format("truetype");
}

@font-face {
font-family: \${FONT_FAMILY_MAP[2 /* Resizable */]};
font-family: FluentSystemIconsLight;
font-display: "block";
src: url(\${JSON.stringify(fontOneSizeWoff2)}) format("woff2"),
url(\${JSON.stringify(fontOneSizeWoff)}) format("woff"),
url(\${JSON.stringify(fontOneSizeTtf)}) format("truetype");
src:
url(\${JSON.stringify(fontLightWoff2)}) format("woff2"),
url(\${JSON.stringify(fontLightWoff)}) format("woff"),
url(\${JSON.stringify(fontLightTtf)}) format("truetype");
}
\`);
// #endregion @generated font-faces
export const useRootStyles = makeStyles({
root: {
display: 'inline-block',
Expand All @@ -425,6 +421,7 @@ describe('Build Verification', () => {
lineHeight: '1em',
color: 'currentColor',
},
// #region @generated font-families
[0 /* Filled */]: {
fontFamily: 'FluentSystemIconsFilled',
},
Expand Down Expand Up @@ -2024,7 +2021,7 @@ describe('Build Verification', () => {
const fontsPath = path.join(__dirname, 'lib', 'utils', 'fonts');

// Check for different font types
const fontTypes = ['Regular', 'Filled', 'Light', 'Resizable'];
const fontTypes = FONT_FAMILIES.map(({ key }) => key);
const fontExtensions = ['json', 'ttf', 'woff', 'woff2'];

for (const fontType of fontTypes) {
Expand Down Expand Up @@ -2056,6 +2053,32 @@ describe('Build Verification', () => {
expect(fs.existsSync(filePath)).toBe(true);
}
});

// A family declared on only one of the two surfaces renders as no font at all. This
// asserts the shipped artifacts agree with each other and with scripts/font-families.js.
it('should declare the same font families in the Griffel and headless outputs', () => {
const expected = FONT_FAMILIES.map(({ family }) => family).sort();

const griffel = fs.readFileSync(path.join(__dirname, 'lib/utils/fonts/createFluentFontIcon.styles.js'), 'utf8');
const headless = fs.readFileSync(path.join(__dirname, 'lib/headless/fonts/styles.css'), 'utf8');

/** @param {string} source */
const fontFaceFamilies = (source) =>
[...source.matchAll(/@font-face\s*\{[^}]*?font-family:\s*'?([\w-]+)'?\s*;/g)].map((match) => match[1]).sort();

expect(fontFaceFamilies(griffel)).toEqual(expected);
expect(fontFaceFamilies(headless)).toEqual(expected);

// …and the same families are selectable from both APIs.
const variants = fs.readFileSync(path.join(__dirname, 'lib/headless/styles.css'), 'utf8');
const griffelRules = [...griffel.matchAll(/\.[\w-]+\{font-family:([\w-]+);\}/g)].map((match) => match[1]).sort();
const headlessRules = [...variants.matchAll(/\[data-fui-icon-font=[^\]]+\]\s*\{\s*font-family:\s*'([\w-]+)'/g)]
.map((match) => match[1])
.sort();

expect(griffelRules).toEqual(expected);
expect(headlessRules).toEqual(expected);
});
});

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/react-icons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
"convert:fonts": "node scripts/convert-font.js --source=./src/utils/fonts --dest=./src/fonts --perIconDest=./src/atoms/fonts --headless --headlessPerIconDest=./src/atoms/headless-fonts --codepointDest=./src/utils/fonts --rtl=./intermediate/rtl.json --metadata=./tmp/metadata-font.json",
"convert:merge-metadata": "node scripts/merge-metadata.js --svgMetadata=./tmp/metadata-svg.json --fontMetadata=./tmp/metadata-font.json --output=./metadata.json",
"generate:font": "node scripts/copy-base-fonts.js --source=../../fonts --dest=src/utils/fonts",
"generate:font-styles": "node scripts/generate-font-styles.js --dest=./src",
"generate:rtl": "node ../../importer/rtlMetadata.js --source=../../assets --dest=./intermediate/rtl.json",
"build:fonts-and-svg": "yarn generate:base-svg && yarn generate:font && yarn generate:rtl",
"build:fonts-and-svg": "yarn generate:base-svg && yarn generate:font && yarn generate:font-styles && yarn generate:rtl",
"build:generate-chunks-and-atoms": "yarn convert:svg && yarn convert:fonts && yarn convert:merge-metadata",
"build:js": "node scripts/build.js",
"build": "yarn clean && yarn build:fonts-and-svg && yarn build:generate-chunks-and-atoms && yarn build:js",
Expand Down
11 changes: 5 additions & 6 deletions packages/react-icons/scripts/convert-font.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const {
getCreateFluentIconHeader,
generatePerIconFiles,
} = require('./convert-font.utils');
const { FONT_FAMILIES, getFontFamilyByKey } = require('./font-families');

if (require.main === module) {
main().catch((err) => {
Expand Down Expand Up @@ -157,12 +158,10 @@ async function processPerChunk(dest, iconEntries, rtlMetadata) {
* @returns {{ resizable: IconEntry[]; sized: IconEntry[]; }}
*/
function prepareProcessedCodepointMap(srcPath, destFolder) {
const fileNamesResizable = ['FluentSystemIcons-Resizable.json'];
const fileNamesSized = [
'FluentSystemIcons-Light.json',
'FluentSystemIcons-Filled.json',
'FluentSystemIcons-Regular.json',
];
// Order is load-bearing: it decides chunk assignment for the generated sized icons.
const SIZED_ORDER = ['Light', 'Filled', 'Regular'];
const fileNamesResizable = FONT_FAMILIES.filter((f) => f.resizable).map((f) => `${f.file}.json`);
const fileNamesSized = SIZED_ORDER.map((key) => `${getFontFamilyByKey(key).file}.json`);

const resolveExistingFiles = (/** @type {string[]} */ names) =>
names.map((name) => path.resolve(srcPath, name)).filter((f) => fsS.existsSync(f));
Expand Down
9 changes: 7 additions & 2 deletions packages/react-icons/scripts/convert-font.utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const path = require('path');
const _ = require('lodash');

const { writePerIconFiles } = require('./per-icon.writer');
const { getFontFamilyByKey } = require('./font-families');

/** @typedef {{ [key: string]: 'mirror' | 'unique' }} RtlMetadata */

Expand Down Expand Up @@ -39,10 +40,14 @@ function loadRtlMetadata(rtlFilePath) {
* @param {string=} rawGlyphName Used to derive size for sized variants
*/
function buildFontIconExport(exportName, codepoint, resizable, flipInRtl, rawGlyphName) {
const style = /filled$/i.test(rawGlyphName || '') ? 0 : /regular$/i.test(rawGlyphName || '') ? 1 : 3; // Light = 3
const style = /filled$/i.test(rawGlyphName || '')
? getFontFamilyByKey('Filled').value
: /regular$/i.test(rawGlyphName || '')
? getFontFamilyByKey('Regular').value
: getFontFamilyByKey('Light').value;
const sizeMatch = rawGlyphName && /(?<=_)\d+(?=_filled|_regular|_light)/.exec(rawGlyphName);
const size = resizable ? undefined : sizeMatch?.[0];
return `export const ${exportName}: FluentFontIcon = (/*#__PURE__*/createFluentFontIcon(${JSON.stringify(exportName)}, ${JSON.stringify(String.fromCodePoint(codepoint))}, ${resizable ? 2 : style}, ${resizable ? undefined : size}${flipInRtl ? ', { flipInRtl: true }' : ''}));`;
return `export const ${exportName}: FluentFontIcon = (/*#__PURE__*/createFluentFontIcon(${JSON.stringify(exportName)}, ${JSON.stringify(String.fromCodePoint(codepoint))}, ${resizable ? getFontFamilyByKey('Resizable').value : style}, ${resizable ? undefined : size}${flipInRtl ? ', { flipInRtl: true }' : ''}));`;
}

/**
Expand Down
10 changes: 4 additions & 6 deletions packages/react-icons/scripts/copy-base-fonts.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ const fs = require('node:fs');
const path = require('node:path');
const { parseArgs } = require('node:util');

const FONT_TYPES = ['Regular', 'Filled', 'Light', 'Resizable'];
const BINARY_EXTENSIONS = ['ttf', 'woff', 'woff2'];
const { FONT_FAMILIES, FONT_BINARY_EXTENSIONS } = require('./font-families');

if (require.main === module) {
try {
Expand Down Expand Up @@ -61,10 +60,9 @@ function main() {
fs.mkdirSync(destDir, { recursive: true });

let copied = 0;
for (const type of FONT_TYPES) {
const baseName = `FluentSystemIcons-${type}`;
for (const ext of [...BINARY_EXTENSIONS, 'json']) {
const fileName = `${baseName}.${ext}`;
for (const { file } of FONT_FAMILIES) {
for (const ext of [...FONT_BINARY_EXTENSIONS, 'json']) {
const fileName = `${file}.${ext}`;
const srcPath = path.join(sourceDir, fileName);
if (!fs.existsSync(srcPath)) {
throw new Error(`Expected font file missing in source: ${srcPath}`);
Expand Down
85 changes: 85 additions & 0 deletions packages/react-icons/scripts/font-families.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// @ts-check
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

/**
* Single source of truth for the icon font families.
*
* Everything that names a font family, a font file, a `data-fui-icon-font` value or a
* `FontFile` enum member is derived from this list — the Griffel `@font-face` block, the
* headless CSS, the generated per-icon atoms and the font copy step. Keeping them in one
* place is what makes the headless and non-headless surfaces provably 1:1.
*
* @typedef {object} FontFamilyDefinition
* @property {string} key `FontFile` enum member name.
* @property {number} value `FontFile` enum value. BAKED INTO GENERATED ATOMS — never renumber.
* @property {string} family CSS `font-family` name.
* @property {string} variant `data-fui-icon-font` attribute value (headless).
* @property {string} file Font binary base name, without extension.
* @property {boolean} resizable Whether this family holds the size-agnostic glyphs.
*/

/** @type {ReadonlyArray<FontFamilyDefinition>} */
const FONT_FAMILIES = [
{
key: 'Filled',
value: 0,
family: 'FluentSystemIconsFilled',
variant: 'filled',
file: 'FluentSystemIcons-Filled',
resizable: false,
},
{
key: 'Regular',
value: 1,
family: 'FluentSystemIconsRegular',
variant: 'regular',
file: 'FluentSystemIcons-Regular',
resizable: false,
},
{
key: 'Resizable',
value: 2,
family: 'FluentSystemIcons',
variant: 'resizable',
file: 'FluentSystemIcons-Resizable',
resizable: true,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

what is this resizable about ?

},
{
key: 'Light',
value: 3,
family: 'FluentSystemIconsLight',
variant: 'light',
file: 'FluentSystemIcons-Light',
resizable: false,
},
];

/** Extensions of the font binaries shipped for every family, in `@font-face` `src` order. */
const FONT_BINARY_EXTENSIONS = /** @type {const} */ (['woff2', 'woff', 'ttf']);

/** CSS `format()` keyword per binary extension. */
const FONT_FORMAT_BY_EXTENSION = /** @type {const} */ ({
woff2: 'woff2',
woff: 'woff',
ttf: 'truetype',
});

/**
* @param {string} key
* @returns {FontFamilyDefinition}
*/
function getFontFamilyByKey(key) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

this should be string union rather string

const found = FONT_FAMILIES.find((definition) => definition.key === key);
if (!found) {
throw new Error(`[font-families] unknown font family key: ${key}`);
}
return found;
}

module.exports = {
FONT_FAMILIES,
FONT_BINARY_EXTENSIONS,
FONT_FORMAT_BY_EXTENSION,
getFontFamilyByKey,
};
Loading