Skip to content

Commit

Permalink
feat: figma icon importer script (#2163)
Browse files Browse the repository at this point in the history
  • Loading branch information
anuraghazra authored May 10, 2024
1 parent c481e15 commit 64108d7
Show file tree
Hide file tree
Showing 1,008 changed files with 17,257 additions and 6,374 deletions.
5 changes: 5 additions & 0 deletions .changeset/olive-years-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@razorpay/blade": minor
---

feat(blade): add new icons and add figma icon importer script
8 changes: 6 additions & 2 deletions packages/blade/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@
"publish-npm": "node ./scripts/publishToNpm.js",
"pregenerate-bundle-size-info": "yarn run-s build:clean build:generate-types build:react-prod",
"generate-bundle-size-info": "node ./scripts/generateBundleSizeInfo.js",
"generate-github-npmrc": "node ./scripts/generateGitHubRegistryNpmrc.js"
"generate-github-npmrc": "node ./scripts/generateGitHubRegistryNpmrc.js",
"generate-icons": "node ./scripts/generateIcons.mjs"
},
"dependencies": {
"@babel/runtime": "7.20.0",
Expand Down Expand Up @@ -276,7 +277,10 @@
"@types/body-scroll-lock": "3.1.0",
"ramda": "0.29.1",
"@razorpay/i18nify-js": "1.9.3",
"@razorpay/i18nify-react": "4.0.8"
"@razorpay/i18nify-react": "4.0.8",
"plop": "3.1.1",
"node-plop": "0.32.0",
"svgson": "5.3.1"
},
"peerDependencies": {
"react": ">=18",
Expand Down
1 change: 1 addition & 0 deletions packages/blade/plop/icon/index.ts.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './{{name}}Icon';
11 changes: 11 additions & 0 deletions packages/blade/plop/icon/{{name}}Icon.native.test.tsx.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {{name}}Icon from '.';
import renderWithTheme from '~utils/testing/renderWithTheme.native';

describe('<{{name}}Icon />', () => {
it('should render {{name}}Icon', () => {
const renderTree = renderWithTheme(
<{{name}}Icon color="feedback.icon.neutral.intense" size="large" />,
).toJSON();
expect(renderTree).toMatchSnapshot();
});
});
13 changes: 13 additions & 0 deletions packages/blade/plop/icon/{{name}}Icon.tsx.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { IMPORTED_SVG_COMPONENTS } from '../_Svg';
import type { IconComponent } from '..';
import useIconProps from '../useIconProps';

const {{name}}Icon: IconComponent = ({ size, color, ...styledProps }) => {
const { height, width, iconColor } = useIconProps({ size, color });

return (
REPLACE_SVG
);
};

export default {{name}}Icon;
11 changes: 11 additions & 0 deletions packages/blade/plop/icon/{{name}}Icon.web.test.tsx.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {{name}}Icon from './';
import renderWithTheme from '~utils/testing/renderWithTheme.web';

describe('<{{name}}Icon />', () => {
it('should render {{name}}Icon', () => {
const { container } = renderWithTheme(
<{{name}}Icon color="feedback.icon.neutral.intense" size="large" />,
);
expect(container).toMatchSnapshot();
});
});
10 changes: 10 additions & 0 deletions packages/blade/plop/iconMap.ts.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This file is auto generated
// Modify at blade/plop/icon/iconMap.ts.hbs
{{{iconImports}}}
import type { IconComponent } from './';

const iconMap: Record<string, IconComponent> = {
{{{iconMap}}}
};

export default iconMap;
4 changes: 4 additions & 0 deletions packages/blade/plop/iconReexports.ts.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// This file is auto generated
// Modify at blade/plop/icon/iconReexports.ts.hbs
export * from './types';
{{{iconReexports}}}
188 changes: 188 additions & 0 deletions packages/blade/plopfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/* eslint-disable prefer-const */
/* eslint-disable import/no-extraneous-dependencies */
const fs = require('fs');
const { stringify, parseSync } = require('svgson');
const { startCase } = require('lodash');
const prettier = require('prettier');

const transformSvgNode = (node, components = new Set()) => {
if (node.name === 'svg') {
node.attributes = {
styledProps: '',
width: '{width}',
height: '{height}',
viewBox: '0 0 24 24',
fill: 'none',
};
}

// title case component names
node.name = startCase(node.name).replace(/\s/g, '');
// gather imported components
components.add(node.name);

// update iconColor in stroke & fill
Object.keys(node.attributes).forEach((attribute) => {
if (['stroke', 'fill'].includes(attribute) && node.attributes[attribute] !== 'none') {
node.attributes[attribute] = `{iconColor}`;
}
});

// recursively go to child
if (node.children) node.children.forEach((child) => transformSvgNode(child, components));

return { node, components };
};

/**
* @param {import("plop").NodePlopAPI} plop
*/
module.exports = (plop) => {
plop.setGenerator('generate-reexports', {
description: 'Generates re-exports for all icon components',
prompts: [],
actions: () => {
const actions = [];

// get all icon components
const iconsFolder = './src/components/Icons';
const icons = fs.readdirSync(iconsFolder);
const allIcons = icons
.map((icon) => {
if (!fs.statSync(`${iconsFolder}/${icon}`).isDirectory()) return null;
const files = fs.readdirSync(`${iconsFolder}/${icon}`);
if (files.length === 0) return null;
if (icon.endsWith('Icon')) {
return icon;
}
return null;
})
.filter(Boolean)
.sort();

const imports = allIcons
.map((icon) => {
return `import ${icon}Component from './${icon}';`;
})
.join('\n');

const map = allIcons
.map((icon) => {
return ` ${icon}: ${icon}Component,`;
})
.join('\n');

const reexports = allIcons
.map((icon) => {
return `export { default as ${icon} } from './${icon}';`;
})
.join('\n');

actions.push({
type: 'add',
path: './src/components/Icons/iconMap.ts',
templateFile: 'plop/iconMap.ts.hbs',
data: {
iconMap: map,
iconImports: imports,
},
force: true,
});

actions.push({
type: 'add',
path: './src/components/Icons/index.ts',
templateFile: 'plop/iconReexports.ts.hbs',
data: {
iconReexports: reexports,
},
force: true,
});

return actions;
},
});

plop.setGenerator('generate-icons', {
description: 'Generates a icon component',
prompts: [
{
type: 'input',
name: 'iconName',
message: 'Enter icon name:',
},
{
type: 'input',
name: 'svgContents',
message: 'Paste svg contents:',
validate: (value) => !!value,
},
],
actions: (answers) => {
const actions = [];

let { iconName, svgContents } = answers;

let name = startCase(iconName).trim().replace(/\s/g, '');
// populate the template code
actions.push({
type: 'addMany',
templateFiles: 'plop/icon/**',
destination: `./src/components/Icons/{{name}}Icon`,
base: 'plop/icon',
data: { name },
abortOnFail: true,
force: true,
});

// modify svg -> jsx
actions.push({
type: 'modify',
path: `./src/components/Icons/{{name}}Icon/{{name}}Icon.tsx`,
data: { name },
transform(fileContents) {
let final = fileContents;
let importedComponents = [];

// parse svg contents to ast and modify the ast with transformSvgNode
const svgAst = parseSync(svgContents, {
camelcase: true,
// transform each node and gather imported components
transformNode: (transformNode) => {
const { node, components } = transformSvgNode(transformNode);
importedComponents = [...components];
return node;
},
});

// stringify svg ast
const svgString = stringify(svgAst, {
selfClose: true,
// transform jsx props
transformAttr: (key, value) => {
if (key === 'styledProps') {
return '{...styledProps}';
}
if (value.startsWith('{')) {
return `${key}=${value}`;
}
return `${key}="${value}"`;
},
});

// replace template svg placeholder
final = final.replace(/REPLACE_SVG/g, svgString);
// update imported svg components
final = final.replace(/IMPORTED_SVG_COMPONENTS/g, importedComponents.join(', '));

return prettier.format(final, {
parser: 'typescript',
singleQuote: true,
});
},
});

return actions;
},
});
};
28 changes: 28 additions & 0 deletions packages/blade/scripts/generateIcons.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-disable import/no-extraneous-dependencies */
import fs from 'node:fs';
import nodePlop from 'node-plop';

const generateIcons = async () => {
const plop = await nodePlop('./plopfile.js');
const iconGenerator = plop.getGenerator('generate-icons');
const indexGenerator = plop.getGenerator('generate-reexports');
const iconsJsonFile = JSON.parse(fs.readFileSync('./scripts/icons.json', 'utf-8'));

const processedIcons = iconsJsonFile.map((icon) => {
const name = Object.keys(icon)[0];
const svg = icon[name];
return iconGenerator.runActions({ iconName: name, svgContents: svg }).then((results) => {
console.log(results);
});
});

Promise.all(processedIcons);
// wait 1 second
await new Promise((resolve) => setTimeout(resolve, 1000));
// generate re-exports
await indexGenerator.runActions({}).then((results) => {
console.log(results);
});
};

generateIcons();
Loading

0 comments on commit 64108d7

Please sign in to comment.