@ikona/cli
Version:
258 lines (251 loc) • 7.64 kB
JavaScript
// src/icons/build.ts
import crypto from "crypto";
import fsExtra2 from "fs-extra";
import { glob } from "glob";
import { parse } from "node-html-parser";
import * as path from "node:path";
// src/utils/validations.ts
import fsExtra from "fs-extra";
import { join, extname } from "node:path";
function clear(folderPath) {
fsExtra.readdir(folderPath, (err, files) => {
if (err) {
console.error("Error reading folder:", err);
return;
}
const svgFiles = files.filter(
(file) => extname(file).toLowerCase() === ".svg" && file.startsWith("sprite")
);
svgFiles.forEach((svgFile) => {
const filePath = join(folderPath, svgFile);
fsExtra.unlink(filePath, (err2) => {
if (err2) {
console.error(`Error removing file ${filePath}:`, err2);
} else {
console.log(`Removed file: ${filePath}`);
}
});
});
});
}
async function writeIfChanged({
filepath,
newContent,
hash,
force
}) {
let _filepath = filepath;
if (hash) {
_filepath = filepath.replace(/\.svg$/, `.${hash}.svg`);
}
const currentContent = await fsExtra.readFile(_filepath, "utf8").catch(() => "");
const shouldSkip = currentContent === newContent && force !== true;
if (shouldSkip)
return false;
if (hash) {
const folder = filepath.replace(/sprite\.svg$/, ``);
clear(folder);
}
await fsExtra.writeFile(_filepath, newContent, "utf8");
return true;
}
// src/utils/config.ts
import { bundleNRequire } from "bundle-n-require";
import findUp from "escalade/sync";
var defaultConfig = {
outputDir: ".ikona"
};
var configs = [".ts", ".js", ".mts", ".mjs", ".cts", ".cjs"];
var configRegex = new RegExp(`ikona.config(${configs.join("|")})$`);
// src/icons/build.ts
import { loadConfig, optimize } from "svgo";
// src/utils/file.ts
function calculateFileSizeInKB(str) {
const buffer = Buffer.from(str, "utf-8");
const fileSizeInKB = buffer.length / 1024;
return fileSizeInKB;
}
// src/icons/build.ts
async function generateIconFiles({
files,
inputDir,
outputDir,
spriteOutputDir,
shouldOptimize,
shouldHash,
force
}) {
const spriteFilepath = path.join(spriteOutputDir, "sprite.svg");
const typeOutputFilepath = path.join(outputDir, "types", "icon-name.d.ts");
const currentSprite = await fsExtra2.readFile(spriteFilepath, "utf8").catch(() => "");
const currentTypes = await fsExtra2.readFile(typeOutputFilepath, "utf8").catch(() => "");
const iconNames = files.map((file) => iconName(file));
const spriteUpToDate = iconNames.every(
(name) => currentSprite.includes(`id=${name}`)
);
const typesUpToDate = iconNames.every(
(name) => currentTypes.includes(`"${name}"`)
);
if (spriteUpToDate && typesUpToDate) {
console.log(`Icons are up to date`);
return;
}
let output = await generateSvgSprite({
files,
inputDir
});
if (shouldOptimize) {
const config = await loadConfig() || void 0;
output = optimize(output, config).data;
}
let hash;
if (shouldHash) {
hash = crypto.createHash("md5").update(output).digest("hex");
}
const spriteChanged = await writeIfChanged({
filepath: spriteFilepath,
newContent: output,
hash,
force
});
if (spriteChanged) {
console.log(`Generating sprite for ${inputDir}`);
for (const file of files) {
console.log("\u2705", file);
}
console.log(`File size: ${calculateFileSizeInKB(output)} KB`);
if (shouldHash) {
console.log(`Generated sprite with hash ${hash}`);
console.log(
`Saved to ${path.relative(
process.cwd(),
spriteFilepath.replace(/\.svg$/, `.${hash}.svg`)
)}`
);
} else {
console.log(`Saved to ${path.relative(process.cwd(), spriteFilepath)}`);
}
}
const stringifiedIconNames = iconNames.map((name) => JSON.stringify(name));
const typeOutputContent = `export type IconName =
| ${stringifiedIconNames.join("\n | ").replace(/"/g, "'")};
`;
const typesChanged = await writeIfChanged({
filepath: typeOutputFilepath,
newContent: typeOutputContent,
force
});
if (typesChanged) {
console.log(
`Types saved to ${path.relative(process.cwd(), typeOutputFilepath)}`
);
}
const iconsOutputFilepath = path.join(outputDir, "icons.ts");
const iconsOutputContent = `import { IconName } from './types/icon-name';
export const icons = [
${stringifiedIconNames.join(",\n ")},
] satisfies Array<IconName>;
`;
const iconsChanged = await writeIfChanged({
filepath: iconsOutputFilepath,
newContent: iconsOutputContent,
force
});
if (iconsChanged) {
console.log(
`Icons names saved to ${path.relative(
process.cwd(),
iconsOutputFilepath
)}`
);
}
if (shouldHash) {
const hashOutputFilepath = path.join(outputDir, "hash.ts");
const hashFileContent = `export const hash = '${hash}';
`;
const hashFileChanged = await writeIfChanged({
filepath: hashOutputFilepath,
newContent: hashFileContent,
force
});
if (hashFileChanged) {
console.log(
`Hash file saved to ${path.relative(process.cwd(), hashOutputFilepath)}`
);
}
}
if (spriteChanged || typesChanged || iconsChanged) {
console.log(`Generated ${files.length} icons`);
} else {
console.log(`Icons are up to date`);
}
}
function iconName(file) {
return file.replace(/\.svg$/, "").replace(/\\/g, "/");
}
async function generateSvgSprite({
files,
inputDir
}) {
const symbols = await Promise.all(
files.map(async (file) => {
const input = await fsExtra2.readFile(path.join(inputDir, file), "utf8");
const root = parse(input);
const svg = root.querySelector("svg");
if (!svg)
throw new Error("No SVG element found");
svg.tagName = "symbol";
svg.setAttribute("id", iconName(file));
svg.removeAttribute("xmlns");
svg.removeAttribute("xmlns:xlink");
svg.removeAttribute("version");
svg.removeAttribute("width");
svg.removeAttribute("height");
return svg.toString().trim();
})
);
return [
`<?xml version="1.0" encoding="UTF-8"?>`,
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="0" height="0">`,
`<defs>`,
// for semantics: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs
...symbols,
`</defs>`,
`</svg>`,
""
// trailing newline
].join("\n");
}
async function generateSprite(cliConfig, config) {
const outputDir = cliConfig["out-dir"] || config.outputDir || defaultConfig.outputDir;
const { icons, force } = config;
const { inputDir, spriteOutputDir, optimize: optimize2, hash } = icons;
const cwd = process.cwd();
const inputDirRelative = path.relative(cwd, inputDir);
const outputDirRelative = path.join(cwd, outputDir);
const spriteOutputDirRelative = path.join(cwd, spriteOutputDir);
await Promise.all([
fsExtra2.ensureDir(inputDirRelative),
fsExtra2.ensureDir(outputDirRelative),
fsExtra2.ensureDir(spriteOutputDirRelative)
]);
const files = glob.sync("**/*.svg", {
cwd: inputDirRelative
}).sort((a, b) => a.localeCompare(b));
if (files.length === 0) {
console.log(`No SVG files found in ${inputDirRelative}`);
} else {
await generateIconFiles({
files,
inputDir: inputDirRelative,
outputDir: outputDirRelative,
spriteOutputDir: spriteOutputDirRelative,
shouldOptimize: cliConfig.optimize || optimize2,
shouldHash: cliConfig.hash || hash,
force: cliConfig.force || force
});
}
}
export {
generateSprite
};