UNPKG

@ikona/cli

Version:
560 lines (530 loc) 17.7 kB
#!/usr/bin/env node "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // src/cli.ts var import_commander = require("commander"); // ../../package.json var name = "ikona"; var version = "0.0.1"; var description = "The repo of ikona, a CLI tool for generating SVG sprites from a directory of SVG files and more."; // src/icons/generate-sprite.ts var import_fs_extra4 = __toESM(require("fs-extra")); var import_glob = require("glob"); // src/icons/generate-icon-files.ts var import_crypto = __toESM(require("crypto")); var import_fs_extra3 = __toESM(require("fs-extra")); var path2 = __toESM(require("path")); // src/utils/validations.ts var import_fs_extra = __toESM(require("fs-extra")); var import_node_path = require("path"); // src/utils/hash.ts function addHashToSpritePath(path5, hash) { return path5.replace(/\.svg$/, `.${hash}.svg`); } // src/utils/validations.ts function clear(folderPath) { import_fs_extra.default.readdir(folderPath, (err, files) => { if (err) { console.error("Error reading folder:", err); return; } const svgFiles = files.filter( (file) => (0, import_node_path.extname)(file).toLowerCase() === ".svg" && file.startsWith("sprite") ); svgFiles.forEach((svgFile) => { const filePath = (0, import_node_path.join)(folderPath, svgFile); import_fs_extra.default.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 = addHashToSpritePath(filepath, hash); } const currentContent = await import_fs_extra.default.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 import_fs_extra.default.writeFile(_filepath, newContent, "utf8"); return true; } // src/utils/file.ts function calculateFileSizeInKB(str) { const buffer = Buffer.from(str, "utf-8"); const fileSizeInKB = buffer.length / 1024; return fileSizeInKB; } // src/icons/templates/svg-sprite.ts var import_node_html_parser = require("node-html-parser"); function svgSpriteTemplate(iconsData) { const symbols = iconsData.map((iconData) => { const input = iconData.content; const root = (0, import_node_html_parser.parse)(input); const svg = root.querySelector("svg"); if (!svg) throw new Error("No SVG element found"); svg.tagName = "symbol"; svg.setAttribute("id", iconData.name); svg.removeAttribute("xmlns"); svg.removeAttribute("xmlns:xlink"); svg.removeAttribute("version"); svg.removeAttribute("width"); svg.removeAttribute("height"); svg.removeAttribute("fill"); 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" viewBox="0 0 0 0" 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"); } // src/icons/icon-name.ts function iconName(file) { return file.replace(/\.svg$/, "").replace(/\\/g, "/"); } // src/icons/get-icons-data.ts var import_fs_extra2 = __toESM(require("fs-extra")); var import_node_path2 = __toESM(require("path")); function getIconsData({ files, inputDir }) { return files.map((file) => { const name2 = iconName(file); const content = import_fs_extra2.default.readFileSync(import_node_path2.default.join(inputDir, file), "utf8"); return { name: name2, content }; }); } // src/icons/templates/type.ts var typeTemplate = (iconNames) => `export type IconName = | ${iconNames.join("\n | ").replace(/"/g, "'")}; `; // src/icons/templates/icons.ts var iconsTemplate = (iconNames) => `import { IconName } from './types/icon-name'; export const icons = [ ${iconNames.join(",\n ")}, ] satisfies Array<IconName>; `; // src/icons/templates/hash.ts var hashTemplate = (hash) => `export const hash = '${hash}'; `; // src/utils/optimize.ts var import_svgo = require("svgo"); var defaultSVGOConfig = { multipass: true, plugins: [ { name: "preset-default", params: { overrides: { removeHiddenElems: false, removeUselessDefs: false, cleanupIds: false, convertColors: { currentColor: true }, removeViewBox: false } } } ], js2svg: { indent: 2, pretty: true } }; function optimize(output, options2 = defaultSVGOConfig) { return (0, import_svgo.optimize)(output, options2).data; } // src/icons/generate-icon-files.ts async function generateIconFiles({ files, context }) { const { spriteFilepath, typeOutputFilepath, iconsPath, hashPath, inputDir, shouldOptimize, shouldHash, force } = context; const currentSprite = await import_fs_extra3.default.readFile(spriteFilepath, "utf8").catch(() => ""); const currentTypes = await import_fs_extra3.default.readFile(typeOutputFilepath, "utf8").catch(() => ""); const iconNames = files.map((file) => iconName(file)); const spriteUpToDate = iconNames.every( (name2) => currentSprite.includes(`id=${name2}`) ); const typesUpToDate = iconNames.every( (name2) => currentTypes.includes(`"${name2}"`) ); if (spriteUpToDate && typesUpToDate) { console.log(`Icons are up to date`); return { hash: void 0 }; } const iconsData = getIconsData({ files, inputDir }); if (shouldOptimize) { for (const icon of iconsData) { icon.content = optimize(icon.content, context.svgoConfig); } } const output = svgSpriteTemplate(iconsData); let hash; if (shouldHash) { hash = import_crypto.default.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 ${path2.relative( process.cwd(), addHashToSpritePath(spriteFilepath, hash) )}` ); } else { console.log(`Saved to ${path2.relative(process.cwd(), spriteFilepath)}`); } } const stringifiedIconNames = iconNames.map((name2) => JSON.stringify(name2)); const typeOutputContent = typeTemplate(stringifiedIconNames); const typesChanged = await writeIfChanged({ filepath: typeOutputFilepath, newContent: typeOutputContent, force }); if (typesChanged) { console.log( `Types saved to ${path2.relative(process.cwd(), typeOutputFilepath)}` ); } const iconsOutputFilepath = path2.join(iconsPath); const iconsOutputContent = iconsTemplate(stringifiedIconNames); const iconsChanged = await writeIfChanged({ filepath: iconsOutputFilepath, newContent: iconsOutputContent, force }); if (iconsChanged) { console.log( `Icons names saved to ${path2.relative( process.cwd(), iconsOutputFilepath )}` ); } if (shouldHash && hash) { const hashOutputFilepath = path2.join(hashPath); const hashFileContent = hashTemplate(hash); const hashFileChanged = await writeIfChanged({ filepath: hashOutputFilepath, newContent: hashFileContent, force }); if (hashFileChanged) { console.log( `Hash file saved to ${path2.relative(process.cwd(), hashOutputFilepath)}` ); } } if (spriteChanged || typesChanged || iconsChanged) { console.log(`Generated ${files.length} icons`); } else { console.log(`Icons are up to date`); } return { hash }; } // src/icons/context.ts var path3 = __toESM(require("path")); var createIconsContext = (config) => { const { outputDir, icons, force, cwd } = config; const inputDirRelative = path3.join(cwd, icons.inputDir); const outputDirRelative = path3.join(cwd, outputDir); const spriteOutputDirRelative = path3.join(cwd, icons.spriteOutputDir); const spriteFilepath = path3.join(cwd, icons.spriteOutputDir, "sprite.svg"); const typesDir = path3.join(cwd, outputDir, "types"); const typeOutputFilepath = path3.join(typesDir, "icon-name.d.ts"); const iconsPath = path3.join(cwd, outputDir, "icons.ts"); const hashPath = path3.join(cwd, outputDir, "hash.ts"); return { inputDir: inputDirRelative, outputDir: outputDirRelative, spriteOutputDir: spriteOutputDirRelative, spriteFilepath, typesDir, typeOutputFilepath, iconsPath, hashPath, shouldOptimize: icons.optimize, shouldHash: icons.hash, force, svgoConfig: icons.svgoConfig }; }; // src/icons/generate-sprite.ts async function generateSprite(config) { const context = createIconsContext(config); const files = import_glob.glob.sync("**/*.svg", { cwd: context.inputDir }).sort((a, b) => a.localeCompare(b)); if (files.length === 0) { console.log(`No SVG files found in ${context.inputDir}`); } else { await Promise.all([ import_fs_extra4.default.ensureDir(context.outputDir), import_fs_extra4.default.ensureDir(context.spriteOutputDir), import_fs_extra4.default.ensureDir(context.typesDir) ]); await generateIconFiles({ files, context }); } } // src/illustrations/types.ts var import_fs_extra5 = __toESM(require("fs-extra")); var import_glob2 = require("glob"); var path4 = __toESM(require("path")); // src/illustrations/templates/illustrations.ts var illustrationsTemplate = (illustrationNames) => `import { IllustrationPath } from './types/illustration-path'; export const illustrations = [ ${illustrationNames.join(",\n ")}, ] satisfies Array<IllustrationPath>; `; // src/illustrations/templates/paths.ts var pathsTemplate = (illustrationNames) => `export type IllustrationPath = | ${illustrationNames.join("\n | ").replace(/"/g, "'")}; `; // src/utils/glob.ts function getIllustrationsExtensionsGlobPattern(extensions) { return `**/*.{${extensions.join(",")}}`; } // src/illustrations/types.ts async function generateTypes({ files, typeDir, outputDir, force }) { const typeOutputFilepath = path4.join(typeDir, "illustration-path.d.ts"); const currentTypes = await import_fs_extra5.default.readFile(typeOutputFilepath, "utf8").catch(() => ""); const typesUpToDate = files.every( (path5) => currentTypes.includes(`"${path5}"`) ); if (typesUpToDate) { console.log("Illustrations are up to date"); return; } const stringifiedIllustrationNames = files.map( (path5) => JSON.stringify(`/illustrations/${path5}`) ); const typeOutputContent = pathsTemplate(stringifiedIllustrationNames); const typesChanged = await writeIfChanged({ filepath: typeOutputFilepath, newContent: typeOutputContent, force }); if (typesChanged) { for (const file of files) { console.log("\u2705", file); } console.log( `Types saved to ${path4.relative(process.cwd(), typeOutputFilepath)}` ); } const illustrationsOutputFilepath = path4.join(outputDir, "illustrations.ts"); const illustrationsOutputContent = illustrationsTemplate( stringifiedIllustrationNames ); const illustrationsChanged = await writeIfChanged({ filepath: illustrationsOutputFilepath, newContent: illustrationsOutputContent, force }); if (illustrationsChanged) { console.log( `Illustrations saved to ${path4.relative( process.cwd(), illustrationsOutputFilepath )}` ); } if (typesChanged || illustrationsChanged) { console.log(`Generated ${files.length} icons`); } else { console.log(`Illustrations are up to date`); } } async function generateIllustrationTypes(config) { const outputDir = config.outputDir; const { inputDir } = config.illustrations; const cwd = process.cwd(); const inputDirRelative = path4.relative(cwd, inputDir); const outputDirRelative = path4.join(cwd, outputDir); const typeDirRelative = path4.join(cwd, outputDir, "types"); await Promise.all([ import_fs_extra5.default.ensureDir(inputDirRelative), import_fs_extra5.default.ensureDir(outputDirRelative), import_fs_extra5.default.ensureDir(typeDirRelative) ]); const files = import_glob2.glob.sync( getIllustrationsExtensionsGlobPattern(config.illustrations.extensions), { cwd: inputDir } ).sort((a, b) => a.localeCompare(b)); if (files.length === 0) { console.log(`No illustration files found in ${inputDirRelative}`); } else { await generateTypes({ files, typeDir: typeDirRelative, outputDir: outputDirRelative, force: config.force }); } } // src/utils/config.ts var import_bundle_n_require = require("bundle-n-require"); var import_sync = __toESM(require("escalade/sync")); var import_path = require("path"); var defaultConfig = { verbose: false, outputDir: ".ikona", force: false, icons: { optimize: false, inputDir: "icons", spriteOutputDir: "output", hash: false }, illustrations: { inputDir: "illustrations", extensions: ["svg", "png", "jpg", "jpeg", "webp"] }, cwd: process.cwd() }; var configs = [".ts", ".js", ".mts", ".mjs", ".cts", ".cjs"]; var configRegex = new RegExp(`ikona.config(${configs.join("|")})$`); var isConfig = (file) => configRegex.test(file); function findConfigFile({ cwd, file }) { if (file) return (0, import_path.resolve)(cwd, file); return (0, import_sync.default)(cwd, (_dir, paths) => { return paths.find(isConfig); }); } async function bundle(filepath, cwd) { const { mod: config, dependencies } = await (0, import_bundle_n_require.bundleNRequire)(filepath, { cwd, interopDefault: true }); return { config: config?.default ?? config, dependencies }; } var resolveFileConfig = async () => { const currentDir = process.cwd(); const filePath = findConfigFile({ cwd: currentDir }); if (!filePath) { throw new Error("Config file not found"); } const { config } = await bundle(filePath, currentDir); return config; }; // src/utils/merge-config.ts var mergeConfigs = ({ cliConfig, fileConfig }) => { return { verbose: cliConfig.v ?? cliConfig.verbose ?? fileConfig.verbose ?? defaultConfig.verbose, outputDir: cliConfig["out-dir"] ?? fileConfig.outputDir ?? defaultConfig.outputDir, force: cliConfig.force ?? fileConfig.force ?? defaultConfig.force, icons: { optimize: cliConfig.optimize ?? fileConfig.icons?.optimize ?? defaultConfig.icons.optimize, inputDir: fileConfig.icons?.inputDir ?? defaultConfig.icons.inputDir, // TODO add missing cli config flag spriteOutputDir: fileConfig.icons?.spriteOutputDir ?? defaultConfig.icons.spriteOutputDir, // TODO add missing cli config flag hash: cliConfig.hash ?? fileConfig.icons?.hash ?? defaultConfig.icons.hash }, illustrations: { inputDir: fileConfig.illustrations?.inputDir ?? defaultConfig.illustrations.inputDir, // TODO add missing cli config flag extensions: fileConfig.illustrations?.extensions ?? defaultConfig.illustrations.extensions // TODO add missing cli config flag }, cwd: cliConfig.cwd ?? fileConfig.cwd ?? defaultConfig.cwd }; }; // src/index.ts async function init(cliConfig) { const fileConfig = await resolveFileConfig(); const config = mergeConfigs({ cliConfig, fileConfig }); await Promise.all([ generateSprite(config), generateIllustrationTypes(config) ]); } // src/cli.ts var program = new import_commander.Command(); program.name(name).version(version).description(description).option("-v, --verbose", "Verbose output").option("--out-dir <path>", "Output directory").option("--optimize", "Optimize SVGs").option("--force", "Force generation of files").option("--hash", "Hash sprite file name").option("--cwd <path>", "Current working directory").parse(process.argv); var options = program.opts(); init(options);