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

plasma-icons: build-time generate Icon components #1101

Merged
merged 3 commits into from
Mar 12, 2024
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
3 changes: 3 additions & 0 deletions packages/plasma-icons/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/types
/utils
/helpers
/old
/scalable
/index.js*
/index.d.ts*
Expand All @@ -16,5 +17,7 @@
/Icons
/.docz
/es
/scripts-build
/src-build
build-sb
build-storybook.log
9 changes: 5 additions & 4 deletions packages/plasma-icons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@
},
"scripts": {
"prepare": "npm run build",
"prebuild": "rm -rf ./es ./Icon.assets ./Icons index.js ./Icon* ./index.* ./scalable",
"build": "npm run build:cjs && npm run build:esm",
"prebuild": "rm -rf ./es ./Icon* ./index.* ./old ./scalable ./scripts-build ./src-build && cp -r ./src ./src-build",
"build": "npm run generate:react && npm run build:cjs && npm run build:esm",
"postbuild": "tsc --outDir . --emitDeclarationOnly",
"build:cjs": "BABEL_ENV=cjs SC_NAMESPACE=plasma babel ./src --out-dir . --extensions .ts,.tsx",
"build:esm": "BABEL_ENV=esm SC_NAMESPACE=plasma babel ./src --out-dir ./es --extensions .ts,.tsx",
"generate:react": "BABEL_ENV=cjs SC_NAMESPACE=plasma babel ./scripts --out-dir ./scripts-build --extensions .ts,.tsx && node ./scripts-build/generateReactComponents.js",
"build:cjs": "BABEL_ENV=cjs SC_NAMESPACE=plasma babel ./src-build --out-dir . --extensions .ts,.tsx",
"build:esm": "BABEL_ENV=esm SC_NAMESPACE=plasma babel ./src-build --out-dir ./es --extensions .ts,.tsx",
"lint": "../../node_modules/.bin/eslint ./src --ext .js,.ts,.tsx --quiet"
},
"files": [
Expand Down
76 changes: 76 additions & 0 deletions packages/plasma-icons/scripts/generateReactComponents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as fs from 'fs';
import * as path from 'path';
import { getIconAsset, getIconComponent } from './utils';

const rootPath = './src-build/scalable';
const sourceDirectory = `${rootPath}/Icon.svg.24`;
const AssetDirectory16 = `${rootPath}/Icon.assets.16`;
const AssetDirectory24 = `${rootPath}/Icon.assets.24`;
const AssetDirectory36 = `${rootPath}/Icon.assets.36`;

const IconsDirectory = `${rootPath}/Icons`;

const IndexPath = `${rootPath}/index.ts`;
const IconPath = `${rootPath}/Icon.tsx`;

const destinations = [AssetDirectory16, AssetDirectory24, AssetDirectory36, IconsDirectory];

const files = fs.readdirSync(sourceDirectory);

// Создаем директории, если нет
destinations.forEach((destination) => {
if (!fs.existsSync(destination)) {
fs.mkdirSync(destination);
}
});

const names: Array<string> = [];

files.forEach((file) => {
const sourceFilePath = path.join(sourceDirectory, file);
const extension = path.extname(file);

if (extension !== '.svg') {
return;
}

const data = fs.readFileSync(sourceFilePath, 'utf8');

const componentName = path.parse(file).name;
names.push(componentName);
const assetContent24 = getIconAsset(data, componentName);
// заглушка под другие размеры
const assetContentNull = `export const ${componentName} = null;\n`;
const componentContent = getIconComponent(componentName);

const getPath = (dir: string, name: string) => {
return path.join(dir, `${name}.tsx`);
};

// генерируем файлы-ассеты
fs.writeFileSync(getPath(AssetDirectory16, componentName), assetContentNull, 'utf8');
fs.writeFileSync(getPath(AssetDirectory24, componentName), assetContent24, 'utf8');
fs.writeFileSync(getPath(AssetDirectory36, componentName), assetContentNull, 'utf8');
// генерируем компоненты
fs.writeFileSync(getPath(IconsDirectory, `Icon${componentName}`), componentContent, 'utf8');
});

// добавляем экспорты
const indexExport = names
.map((name) => {
return `export { Icon${name} } from '.${IconsDirectory.replace(rootPath, '')}/Icon${name}';`;
})
.join('\n');

fs.appendFileSync(IndexPath, indexExport, 'utf8');

// добавляем к маппингу по категориям
const dataIconFile = fs.readFileSync(IconPath, 'utf8');
const iconImport = names
.map((name) => {
return `import { ${name} } from '.${AssetDirectory24.replace(rootPath, '')}/${name}';`;
})
.join('\n');
const resultIconFile = iconImport + '\n' + dataIconFile.replace(/\'/g, '');

fs.writeFileSync(IconPath, resultIconFile, 'utf8');
72 changes: 72 additions & 0 deletions packages/plasma-icons/scripts/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
const camelize = (source: string) => source.replace(/[-|_]./g, (x) => x[1].toUpperCase());
const compose = <R>(...fns: Array<(a: R) => R>) => (x: R) => fns.reduce((v, f) => f(v), x);
const removeLineBreak = (source: string) => source.replace(/\n/g, '');

const getSvgContent = (source: string) => (/<svg(.*?)>(.*?)<\/svg>/gm.exec(source) || [])[2];

const getViewBox = (source: string) => (/viewBox="(.*?)"/gm.exec(source) || [])[1];

const removeFillOpacity = (source: string) => source.replace(/fill-opacity="(.*?)"/gm, '');

const setFillCurrentColor = (source: string) => source.replace(/fill="(.*?)"/gm, 'fill="currentColor"');

const convertCSSProperty = (source: string) =>
source.replace(
/([a-zA-Z-]*):(.*)/g,
(...match) => `${camelize(match[1])}: ${Number.isNaN(Number(match[2])) ? `'${match[2]}'` : match[2]}`,
);

const getCSSProperties = (source: string) => source.split(';').map(convertCSSProperty).join(',');

const convertInlineStyleToObject = (source: string) =>
source.replace(/style="(.*?)"/gm, (_, group) => `style={{ ${getCSSProperties(group)} }}`);

const camelizeAttributes = (source: string) => source.replace(/([\w-]+)=/g, camelize);

/**
* Функция генерации файла `/Icon.assets/<Name>.tsx`. Здесь экспортируется иконка из figma
* и возвращается svg компонент иконки.
*/
export const getIconAsset = (source: string, iconName: string) => {
const viewBox = getViewBox(source);
const svgContent = compose(
removeLineBreak,
getSvgContent,
setFillCurrentColor,
removeFillOpacity,
convertInlineStyleToObject,
camelizeAttributes,
)(source);

return `import React from 'react';

import { IconProps } from '../IconRoot';

export const ${iconName}: React.FC<IconProps> = (props) => (
<svg width="100%" viewBox="${viewBox}" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
${svgContent}
</svg>
);
`;
};

/**
* Функция генерации файла `/Icons/Icon<Name>.tsx`. Здесь формируется компонент иконки.
*/
export const getIconComponent = (iconName: string) => {
return `import React from 'react';

import { ${iconName} as Icon16 } from '../Icon.assets.16/${iconName}';
import { ${iconName} as Icon24 } from '../Icon.assets.24/${iconName}';
import { ${iconName} as Icon36 } from '../Icon.assets.36/${iconName}';
import { IconProps, IconRoot, getIconComponent, sizeMap } from '../IconRoot';

export const Icon${iconName}: React.FC<IconProps> = ({ size = 's', color, className }) => {
const IconComponent = getIconComponent(Icon16, Icon24, Icon36, sizeMap[size].size);
if (!IconComponent) {
return null;
}
return <IconRoot className={className} size={size} color={color} icon={IconComponent} />;
};
`;
};
Loading
Loading