Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions packages/react-native-icons/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"presets": ["@griffel"],
"compact": false
}
1 change: 1 addition & 0 deletions packages/react-native-icons/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
2 changes: 2 additions & 0 deletions packages/react-native-icons/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@fluentui/react-native-icons
===
175 changes: 175 additions & 0 deletions packages/react-native-icons/convert-font.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// @ts-check

const fs = require("fs/promises");
const path = require("path");
const process = require("process");
const argv = require("yargs").boolean("selector").default("selector", false).argv;
const _ = require("lodash");
const mkdirp = require('mkdirp');
const { promisify } = require('util');
const glob = promisify(require('glob'));

// @ts-ignore
const SRC_PATH = argv.source;
// @ts-ignore
const DEST_PATH = argv.dest;
// @ts-ignore
const CODEPOINT_DEST_PATH = argv.codepointDest;

if (!SRC_PATH) {
throw new Error("Icon source folder not specified by --source");
}
if (!DEST_PATH) {
throw new Error("Output destination folder not specified by --dest");
}
if (!CODEPOINT_DEST_PATH) {
throw new Error("Output destination folder for codepoint map not specified by --dest");
}

processFiles(SRC_PATH, DEST_PATH)

async function processFiles(src, dest) {
/** @type string[] */
const indexContents = [];

// make file for resizeable icons
const iconPath = path.join(dest, 'icons')
const iconContents = await processFolder(src, CODEPOINT_DEST_PATH, true);

await cleanFolder(iconPath);

await Promise.all(iconContents.map(async (chunk, i) => {
const chunkFileName = `chunk-${i}`
const chunkPath = path.resolve(iconPath, `${chunkFileName}.tsx`);
indexContents.push(`export * from './icons/${chunkFileName}'`);
await fs.writeFile(chunkPath, chunk);
}));

// make file for sized icons
const sizedIconPath = path.join(dest, 'sizedIcons');
const sizedIconContents = await processFolder(src, CODEPOINT_DEST_PATH, false)
await cleanFolder(sizedIconPath);

await Promise.all(sizedIconContents.map(async (chunk, i) => {
const chunkFileName = `chunk-${i}`
const chunkPath = path.resolve(sizedIconPath, `${chunkFileName}.tsx`);
indexContents.push(`export * from './sizedIcons/${chunkFileName}'`);
await fs.writeFile(chunkPath, chunk);
}));

const indexPath = path.join(dest, 'index.tsx')
// Finally add the interface definition and then write out the index.
indexContents.push('export { FluentIconsProps } from \'../utils/FluentIconsProps.types\'');
indexContents.push('export { default as wrapIcon } from \'../utils/wrapIcon\'');
indexContents.push('export { default as bundleIcon } from \'../utils/bundleIcon\'');
indexContents.push('export * from \'../utils/useIconState\'');
indexContents.push('export * from \'../utils/constants\'');

await fs.writeFile(indexPath, indexContents.join('\n'));

}

/**
* Process a folder of svg files and convert them to React components, following naming patterns for the FluentUI System Icons
* @param {string} srcPath
* @param {string} codepointMapDestFolder
* @param {boolean} resizable
* @returns { Promise<string[]> } - chunked icon files to insert
*/
async function processFolder(srcPath, codepointMapDestFolder, resizable) {
var files = await glob(resizable ? 'FluentSystemIcons-Resizable.json' : 'FluentSystemIcons-{Filled,Regular}.json', { cwd: srcPath, absolute: true });

/** @type string[] */
const iconExports = [];
await Promise.all(files.map(async (srcFile, index) => {
/** @type {Record<string, number>} */
const iconEntries = JSON.parse(await fs.readFile(srcFile, 'utf8'));
iconExports.push(...generateReactIconEntries(iconEntries, resizable));

return generateCodepointMapForWebpackPlugin(
path.resolve(codepointMapDestFolder, path.basename(srcFile)),
iconEntries,
resizable
);
}));

// chunk all icons into separate files to keep build reasonably fast
/** @type string[][] */
const iconChunks = [];
while (iconExports.length > 0) {
iconChunks.push(iconExports.splice(0, 500));
}

for (const chunk of iconChunks) {
chunk.unshift(`import {createFluentFontIcon} from "../../utils/fonts/createFluentFontIcon";`)
}

/** @type string[] */
const chunkContent = iconChunks.map(chunk => chunk.join('\n'));

return chunkContent;
}

/**
*
* @param {string} destPath
* @param {Record<string,number>} iconEntries
* @param {boolean} resizable
*/
async function generateCodepointMapForWebpackPlugin(destPath, iconEntries, resizable) {
const finalCodepointMap = Object.fromEntries(
Object.entries(iconEntries)
.map(([name, codepoint]) => [getReactIconNameFromGlyphName(name, resizable), codepoint])
);

await fs.writeFile(destPath, JSON.stringify(finalCodepointMap, null, 2));
}

/**
*
* @param {Record<string, number>} iconEntries
* @param {boolean} resizable
* @returns {string[]}
*/
function generateReactIconEntries(iconEntries, resizable) {
/** @type {string[]} */
const iconExports = [];
for (const [iconName, codepoint] of Object.entries(iconEntries)) {
let destFilename = getReactIconNameFromGlyphName(iconName, resizable);

var jsCode = `export const ${destFilename} = /*#__PURE__*/createFluentFontIcon(${JSON.stringify(destFilename)
}, ${JSON.stringify(String.fromCodePoint(codepoint))
}, ${resizable ? 2 /* Resizable */ : /filled$/i.test(iconName) ? 0 /* Filled */ : 1 /* Regular */
}${resizable ? '' : `, ${/(?<=_)\d+(?=_filled|_regular)/.exec(iconName)[0]}`
});`;

iconExports.push(jsCode);
}

return iconExports;
}

/**
*
* @param {string} iconName
* @param {boolean} resizable
* @returns {string}
*/
function getReactIconNameFromGlyphName(iconName, resizable) {
let destFilename = iconName.replace("ic_fluent_", ""); // strip ic_fluent_
destFilename = resizable ? destFilename.replace("20", "") : destFilename;
destFilename = _.camelCase(destFilename); // We want them to be camelCase, so access_time would become accessTime here
destFilename = destFilename.replace(destFilename.substring(0, 1), destFilename.substring(0, 1).toUpperCase()); // capitalize the first letter
return destFilename;
}

async function cleanFolder(folder) {
try {
await fs.access(folder);
await fs.rm(folder, { recursive: true, force: true });
} catch { }

await mkdirp(folder);
}
184 changes: 184 additions & 0 deletions packages/react-native-icons/convert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

const svgr = require("@svgr/core");
const fs = require("fs");
const path = require("path");
const process = require("process");
const argv = require("yargs").boolean("selector").default("selector", false).argv;
const _ = require("lodash");

const SRC_PATH = argv.source;
const DEST_PATH = argv.dest;

const TSX_EXTENSION = '.tsx'

if (!SRC_PATH) {
throw new Error("Icon source folder not specified by --source");
}
if (!DEST_PATH) {
throw new Error("Output destination folder not specified by --dest");
}

if (!fs.existsSync(DEST_PATH)) {
fs.mkdirSync(DEST_PATH);
}

processFiles(SRC_PATH, DEST_PATH)

function processFiles(src, dest) {
/** @type string[] */
const indexContents = [];

// make file for resizeable icons
const iconPath = path.join(dest, 'icons')
const iconContents = processFolder(src, dest, true)

if (fs.existsSync(iconPath)) {
fs.rmSync(iconPath, { recursive: true, force: true } );
}
fs.mkdirSync(iconPath);

iconContents.forEach((chunk, i) => {
const chunkFileName = `chunk-${i}`
const chunkPath = path.resolve(iconPath, `${chunkFileName}.tsx`);
indexContents.push(`export * from './icons/${chunkFileName}'`);
fs.writeFileSync(chunkPath, chunk, (err) => {
if (err) throw err;
});
});

// make file for sized icons
const sizedIconPath = path.join(dest, 'sizedIcons');
const sizedIconContents = processFolder(src, dest, false)
if (fs.existsSync(sizedIconPath)) {
fs.rmSync(sizedIconPath, { recursive: true, force: true } );
}
fs.mkdirSync(sizedIconPath);

sizedIconContents.forEach((chunk, i) => {
const chunkFileName = `chunk-${i}`
const chunkPath = path.resolve(sizedIconPath, `${chunkFileName}.tsx`);
indexContents.push(`export * from './sizedIcons/${chunkFileName}'`);
fs.writeFileSync(chunkPath, chunk, (err) => {
if (err) throw err;
});
});

const indexPath = path.join(dest, 'index.tsx')
// Finally add the interface definition and then write out the index.
indexContents.push('export { FluentReactNativeIconsProps } from \'./utils/FluentReactNativeIconsProps.types\'');
indexContents.push('export { default as wrapIcon } from \'./utils/wrapIcon\'');
indexContents.push('export { default as bundleIcon } from \'./utils/bundleIcon\'');
indexContents.push('export * from \'./utils/useIconState\'');
indexContents.push('export * from \'./utils/constants\'');

fs.writeFileSync(indexPath, indexContents.join('\n'), (err) => {
if (err) throw err;
});

}

/**
* Process a folder of svg files and convert them to React components, following naming patterns for the FluentUI System Icons
* @param {string} srcPath
* @param {boolean} resizable
* @returns { string [] } - chunked icon files to insert
*/
function processFolder(srcPath, destPath, resizable) {
var files = fs.readdirSync(srcPath)

// These options will be passed to svgr/core
// See https://react-svgr.com/docs/options/ for more info
var svgrOpts = {
template: fileTemplate,
expandProps: 'start', // HTML attributes/props for things like accessibility can be passed in, and will be expanded on the svg object at the start of the object
svgProps: { style: '{style}'}, // In RN style attribute is used for styling
replaceAttrValues: { '#212121': '{primaryFill}' }, // We are designating primaryFill as the primary color for filling. If not provided, it defaults to null.
typescript: true,
icon: true
}

var svgrOptsSizedIcons = {
template: fileTemplate,
expandProps: 'start', // HTML attributes/props for things like accessibility can be passed in, and will be expanded on the svg object at the start of the object
svgProps: { style: '{style}'}, // In RN style attribute is used for styling
replaceAttrValues: { '#212121': '{primaryFill}' }, // We are designating primaryFill as the primary color for filling. If not provided, it defaults to null.
typescript: true
}

/** @type string[] */
const iconExports = [];
files.forEach(function (file, index) {
var srcFile = path.join(srcPath, file)
if (fs.lstatSync(srcFile).isDirectory()) {
// for now, ignore subdirectories/localization, until we have a plan for handling it
// Will likely involve appending the lang/locale to the end of the friendly name for the unique component name
// var joinedDestPath = path.join(destPath, file)
// if (!fs.existsSync(joinedDestPath)) {
// fs.mkdirSync(joinedDestPath);
// }
// indexContents += processFolder(srcFile, joinedDestPath)
} else {
if(resizable && !file.includes("20")) {
return
}
var iconName = file.substr(0, file.length - 4) // strip '.svg'
iconName = iconName.replace("ic_fluent_", "") // strip ic_fluent_
iconName = resizable ? iconName.replace("20", "") : iconName
var destFilename = _.camelCase(iconName) // We want them to be camelCase, so access_time would become accessTime here
destFilename = destFilename.replace(destFilename.substring(0, 1), destFilename.substring(0, 1).toUpperCase()) // capitalize the first letter

var iconContent = fs.readFileSync(srcFile, { encoding: "utf8" })

var jsxCode = resizable ? svgr.default.sync(iconContent, svgrOpts, { filePath: file }) : svgr.default.sync(iconContent, svgrOptsSizedIcons, { filePath: file })
var rnRegex = new RegExp('(<(|\/))(svg|path|rect|g)', 'g')
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As majority of our icons contains only those tags I decided to use regex to change casing

var rnCode = jsxCode.replace(rnRegex, function(result) {
var charRegex = new RegExp('[a-zA-Z]');
return result.replace(charRegex, firstSymbol => firstSymbol.toUpperCase()) });
var jsCode =
`

const ${destFilename}Icon = (props: FluentReactNativeIconsProps) => {
const { fill: primaryFill = 'currentColor', style } = props;
return ${rnCode};
}
export const ${destFilename} = /*#__PURE__*/wrapIcon(/*#__PURE__*/${destFilename}Icon, '${destFilename}');
`
iconExports.push(jsCode);
}
});

// chunk all icons into separate files to keep build reasonably fast
/** @type string[][] */
const iconChunks = [];
while(iconExports.length > 0) {
iconChunks.push(iconExports.splice(0, 500));
}

for(const chunk of iconChunks) {
chunk.unshift(`import { Path, Svg, Rect, G } from 'react-native-svg';`)
chunk.unshift(`import wrapIcon from "../utils/wrapIcon";`)
chunk.unshift(`import { FluentReactNativeIconsProps } from "../utils/FluentReactNativeIconsProps.types";`)
chunk.unshift(`import * as React from "react";`)
}

/** @type string[] */
const chunkContent = iconChunks.map(chunk => chunk.join('\n'));

return chunkContent;
}

function fileTemplate(
{ template },
opts,
{ imports, interfaces, componentName, props, jsx, exports }
) {
const plugins = ['jsx', 'typescript']
const tpl = template.smart({ plugins })

componentName.name = componentName.name.substring(3)
componentName.name = componentName.name.replace('IcFluent', '')

return jsx;
}
Loading