Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added package for react-native SVG icons #529

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
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
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