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 5 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": ["@babel/preset-typescript"],
"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
===
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\'');

Choose a reason for hiding this comment

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

Should these commented lines be removed?

Copy link
Author

Choose a reason for hiding this comment

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

Not for now. Maybe I'll need it later. If not, I'll remove it

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;
}
71 changes: 71 additions & 0 deletions packages/react-native-icons/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"name": "@fluentui/react-native-icons",
"version": "0.0.1",
"sideEffects": false,
"main": "lib-cjs/index.js",
"module": "lib/index.js",
"typings": "lib/index.d.ts",
"description": "Fluent System Icons are a collection of familiar, friendly, and modern icons from Microsoft.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/microsoft/fluentui-system-icons.git"
},
"scripts": {
"clean": "find ./src -type f ! -name \"wrapIcon.tsx\" -name \"*.tsx\" -delete",
"cleanSvg": "rm -rf ./intermediate",
"copy": "node ../../importer/generate.js --source=../../assets --dest=./intermediate --extension=svg --target=react",
"convert:svg": "node convert.js --source=./intermediate --dest=./src",
"rollup": "node ./generateRollup.js",
"optimize": "svgo --config svgo.config.js --folder=./intermediate --precision=2",
"unfill": "find ./intermediate -type f -name \"*.svg\" -exec sed -i.bak 's/fill=\"none\"//g' {} \\; && find ./intermediate -type f -name \"*.bak\" -delete",
"build": "npm run copy && npm run convert:svg && npm run cleanSvg && npm run build:esm && npm run build:cjs",
"build:cjs": "tsc --module commonjs --outDir lib-cjs && babel lib-cjs --out-dir lib-cjs",
"build:esm": "tsc && babel lib --out-dir lib"
},
"devDependencies": {
"@babel/cli": "^7.16.0",
"@babel/core": "^7.16.0",
"@babel/preset-typescript": "^7.16.7",
"@svgr/core": "^5.5.0",
"@types/node": "^18.11.18",
"@types/react": "^17.0.2",
"@types/react-native": "^0.68.0",
"cpy-cli": "^4.1.0",
"glob": "^7.2.0",
"lodash": "^4.17.21",
"mkdirp": "^1.0.4",
"react": "^17.0.1",
"react-native": "^0.68.0",
"react-native-svg": "12.5.0",
"renamer": "^2.0.1",
"svgo": "^2.8.0",
"typescript": "~4.1.0",
"yargs": "^14.0.0"
},
"dependencies": {
"tslib": "^2.1.0"
},
"peerDependencies": {
"react": ">=16.8.0 <19.0.0",
"react-native": "^0.68.0"
},
"files": [
"lib/",
"lib-cjs/"
],
"exports": {
".": {
"default": {
"types": "./lib/index.d.ts",
"import": "./lib/index.js",
"require": "./lib-cjs/index.js"
}
},
"./lib/svg": {
"types": "./lib/index.d.ts",
"import": "./lib/index.js",
"require": "./lib-cjs/index.js"
}
}
}
1 change: 1 addition & 0 deletions packages/react-native-icons/src/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fonts/
6 changes: 6 additions & 0 deletions packages/react-native-icons/src/custom.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'react-native-svg';
declare module 'react-native-svg' {
export interface SvgProps {
xmlns?: string;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ImageStyle, StyleProp, TextStyle } from 'react-native';
import { SvgProps } from 'react-native-svg';

export type FluentReactNativeIconsProps = SvgProps & {
primaryFill?: string
style?: StyleProp<ImageStyle | TextStyle>;
filled?: boolean;
title?: string;
}
26 changes: 26 additions & 0 deletions packages/react-native-icons/src/utils/useIconState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { FluentReactNativeIconsProps } from "./FluentReactNativeIconsProps.types";

export const useIconState = (props: FluentReactNativeIconsProps): Omit<FluentReactNativeIconsProps, 'primaryFill'> => {
const { title, primaryFill = "currentColor", ...rest } = props;
const state = {
...rest,
title: undefined,
fill: primaryFill
} as Omit<FluentReactNativeIconsProps, 'primaryFill'>;

// TODO add here styles for RN
//const styles = useRootStyles();
//state.className = mergeClasses(styles.root, state.className);

if (title) {
state['acessibilityLabel'] = title;
}

if (!state['acessibilityLabel'] && !state['accessibilityLabelledBy']) {
state['accessibilityHidden'] = true;
} else {
state['accessibilityRole'] = 'image';
}

return state;
};
14 changes: 14 additions & 0 deletions packages/react-native-icons/src/utils/wrapIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as React from "react";
import { FluentReactNativeIconsProps } from "./FluentReactNativeIconsProps.types";
import { useIconState } from "./useIconState";

const wrapIcon = (Icon: (iconProps: FluentReactNativeIconsProps) => JSX.Element, displayName?: string) => {
const WrappedIcon = (props: FluentReactNativeIconsProps) => {
const state = useIconState(props);
return <Icon {...state} />
}
WrappedIcon.displayName = displayName;
return WrappedIcon;
}

export default wrapIcon;
13 changes: 13 additions & 0 deletions packages/react-native-icons/svgo.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
mergePaths: false
},
},
},
],
};
20 changes: 20 additions & 0 deletions packages/react-native-icons/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"skipLibCheck": true,
"outDir": "lib",
"target": "ES6",
"module": "esnext",
"lib": ["es6", "dom"],
"declaration": true,
"experimentalDecorators": true,
"importHelpers": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true,
"moduleResolution": "node",
"jsx": "react",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"include": ["src", "custom.d.ts"],
"exclude": ["lib", "lib-cjs"]
}