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

perf(icons): optimize icons, scripts, merge declarations #4265

Merged
merged 1 commit into from
Aug 9, 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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ lerna-debug.log*
# Build
dist
bin
packages/icons/src
packages/icons/sprites
packages/icons/src/*
!packages/icons/src/IconBase.tsx
!packages/icons/src/IconSprite.tsx
!packages/icons/src/utils.ts

*.tgz
.nx

Expand Down
2 changes: 1 addition & 1 deletion packages/icons/assets/Check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/icons/assets/Contains.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 3 additions & 7 deletions packages/icons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,12 @@
"url": "https://github.com/lumada-design/hv-uikit-react/issues"
},
"scripts": {
"optimize": "svgo -r -f assets assets",
"convert": "tsx scripts/svgToReact.ts && tsx scripts/svgToSprite.ts",
"prebuild": "run-s clean convert",
"build": "vite build",
"postbuild": "npx cpy \"sprites\" \"dist\"",
"optimize": "svgo -r -f assets assets",
"preconvert": "npx rimraf src",
"convert": "run-s convert:*",
"convert:svg": "tsx scripts/svgToReact.ts",
"convert:sprites": "tsx scripts/svgToSprite.ts",
"convert:copy": "npx cpy \"lib/*\" \"src\"",
"clean": "npx rimraf dist src sprites package",
"clean": "npx rimraf dist sprites package",
"prepare": "npm run convert",
"prepublishOnly": "npm run build && npx clean-publish"
},
Expand Down
12 changes: 4 additions & 8 deletions packages/icons/scripts/generateComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,11 @@ export const generateComponent = (
iconName: string,
colorArray: string[],
viewBox: string,
basePath = "..",
) => {
const palette = colorArray.map((c) => `"${hexColorMap[c] || c}"`).join(",");
const finalOutput = /(\/>.*){2,}/.test(svgOutput)
? `<>${svgOutput}</>`
: svgOutput;

return `import { createHvIcon } from "${basePath}/IconBase";

export const ${iconName} = createHvIcon(
"${iconName}", "${viewBox}", [${palette}],
<>${svgOutput}</>,
);
`;
return `export const ${iconName} = createHvIcon("${iconName}", "${viewBox}", [${palette}], ${finalOutput});\n`;
};
202 changes: 54 additions & 148 deletions packages/icons/scripts/svgToReact.ts
Original file line number Diff line number Diff line change
@@ -1,173 +1,79 @@
/* eslint-disable no-console */
import fs from "node:fs";
import path from "node:path";
import fs from "node:fs/promises";
import { parse, resolve } from "node:path";
import { transform } from "@svgr/core";

import { generateComponent } from "./generateComponent";
import { extractColors, extractSize, replaceFill } from "./utils";

// Resolve arguments
const inputPath = "assets";
const outputPath = "src";

const componentOutputFolder = outputPath
? path.resolve(process.cwd(), outputPath)
: path.resolve(process.cwd());

const knownSubfolders: Record<string, boolean> = {};

const transformToJsx = (svgCode: string) => {
return transform.sync(svgCode, {
const transformToJsx = (fileData: string, iconName: string) => {
const svgJsx = transform.sync(fileData, {
plugins: ["@svgr/plugin-jsx"],
jsxRuntime: "automatic",
// we only care about the JSX part of the generated React component
template: (vars, { tpl }) => tpl`${vars.jsx}`,
});

// TODO: simplify this logic
const viewBox = extractSize(svgJsx);
const colorArray = extractColors(svgJsx);
const jsxSvgFixedColors = replaceFill(svgJsx, colorArray)
// remove svg tag, keeping only the content
.replace(/<svg.*?>(.*?)<\/svg>;/s, "$1");

// Wrap it up in a React component
return generateComponent(jsxSvgFixedColors, iconName, colorArray, viewBox);
};

const writeFile = (processedSVG: string, fileName: string, subFolder = ".") => {
fs.mkdirSync(path.resolve(componentOutputFolder, subFolder), {
recursive: true,
});
/** Converts all SVGs in `inputDir` to JSX & writes result to `outputFile` */
async function convertSvgFiles(inputDir: string, outputFile: string) {
const files = await fs.readdir(resolve(inputDir), { withFileTypes: true });
const svgFiles = files.filter((f) => f.isFile() && f.name.endsWith(".svg"));

const file = path.resolve(
componentOutputFolder,
subFolder,
`${fileName}.tsx`,
const data = await Promise.all(
svgFiles.map(async (f) => ({
name: `${parse(f.name).name}`.replace(/[-.]g/, ""),
data: await fs.readFile(resolve(inputDir, f.name), "utf-8"),
})),
);

fs.writeFile(file, processedSVG, { flag: "w" }, (err) => {
if (err) {
console.error(`Output file ${file} not writable ${err.code}`);
}
});
const headers = `import { createHvIcon } from "./IconBase";`;
const output = data.map((f) => transformToJsx(f.data, f.name)).join("");

const exportName = fileName.split(".").join("");
const exportString = `export { ${exportName} } from "./${fileName}";\n`;

if (subFolder === ".") {
fs.appendFile(
path.resolve(componentOutputFolder, `icons.ts`),
exportString,
() => {},
);
} else {
fs.appendFile(
path.resolve(componentOutputFolder, subFolder, `index.ts`),
exportString,
() => {},
);
}

if (!knownSubfolders[subFolder]) {
knownSubfolders[subFolder] = true;

if (subFolder === ".") {
fs.appendFile(
path.resolve(componentOutputFolder, `index.ts`),
[
`export * from "./IconBase";`,
`export * from "./IconSprite";`,
"\n",
`export * from "./icons";`,
`import * as icons from "./icons";`,
`export { icons };`,
"\n",
].join("\n"),
() => {},
);
} else {
const subFolderName = subFolder.replace("./", "");
fs.appendFile(
path.resolve(componentOutputFolder, subFolder, "..", `index.ts`),
[
`export * from "${subFolder}";`,
`import * as ${subFolderName} from "${subFolder}";`,
`export { ${subFolderName} };`,
"\n",
].join("\n"),
() => {},
);
}
}
};
await fs.writeFile(resolve(outputFile), `${headers}\n${output}`);
}

const runUtil = (
fileToRead: string,
iconName: string,
subFolder = ".",
depth = 0,
) => {
fs.readFile(fileToRead, "utf8", (err, fileData) => {
if (err) {
console.error(err);
return;
} // exit early

let output = transformToJsx(fileData);

const viewBox = extractSize(output);
const colorArray = extractColors(output);

output = replaceFill(output, colorArray)
// remove svg tag, keeping only the content
.replace(/<svg.*?>(.*?)<\/svg>;/s, "$1");

// Wrap it up in a React component
output = generateComponent(
output,
iconName,
colorArray,
viewBox,
`${".".repeat(depth + 1)}`,
);
writeFile(output, iconName, subFolder);
});
};
const makeHeaders = (name: string) => `
export * from "./${name}";
import * as ${name} from "./${name}";
export { ${name} };
`;

const isFolder = (dirPath: string) =>
fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
async function main() {
const inputPath = "assets";
const outputPath = "src";

const processFile = (file: string, subFolder = ".", depth = 0) => {
const extension = path.extname(file);
const fileName = path.basename(file, extension);
await fs.mkdir(resolve(outputPath), { recursive: true });

if (extension === ".svg") {
const componentName = fileName.replace(/[-.]g/, "");
runUtil(file, componentName, subFolder, depth);
}
};
await convertSvgFiles(inputPath, `${outputPath}/icons.tsx`);

const runUtilForJustFilesInDir = (
folder: string,
subFolder = ".",
depth = 0,
) => {
fs.readdir(folder, (err, files) => {
if (err) {
console.log(err);
return;
} // Get out early if not found

files.forEach((file) => {
const filePath = path.resolve(folder, file);
if (!isFolder(filePath)) {
processFile(filePath, subFolder, depth);
} else {
runUtilForJustFilesInDir(
filePath,
`${subFolder}/${path.basename(file)}`,
depth + 1,
);
}
});
});
};
const files = await fs.readdir(inputPath, { withFileTypes: true });
const subDirs = files.filter((f) => f.isDirectory()).map((f) => f.name);

await Promise.all(
subDirs.map((dir) =>
convertSvgFiles(`${inputPath}/${dir}`, `${outputPath}/${dir}.tsx`),
),
);

fs.mkdir(outputPath, { recursive: true }, (err) => {
if (err) throw err;
});
const allDirs = ["icons", ...subDirs];

fs.writeFile(path.resolve(process.cwd(), outputPath, `index.ts`), "", () => {});
const indexFile = `
export * from "./IconBase";
export * from "./IconSprite";
${allDirs.map(makeHeaders).join("")}
`;
await fs.writeFile(resolve("src/index.ts"), indexFile);
}

runUtilForJustFilesInDir(`${process.cwd()}/${inputPath}`);
main();
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion packages/icons/svgo.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module.exports = {
export default {
multipass: true,
quiet: "true",
plugins: [
Expand Down