@wkovacs64/add-icon 0.1.0-dev.14f260a6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Justin R. Hall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # @wkovacs64/add-icon
2
+
3
+ A command-line tool to download icons from the [Iconify Framework](https://iconify.design/) and
4
+ apply custom transformations.
5
+
6
+ ## Installation
7
+
8
+ Add it to your project:
9
+
10
+ ```bash
11
+ npm install @wkovacs64/add-icon
12
+ ```
13
+
14
+ Or use it directly with npx without installing:
15
+
16
+ ```bash
17
+ npx @wkovacs64/add-icon <icon> [options]
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### Basic Usage
23
+
24
+ Download an icon to the specified directory:
25
+
26
+ ```bash
27
+ npx @wkovacs64/add-icon heroicons:arrow-up-circle --output-dir ./app/assets/svg-icons
28
+ ```
29
+
30
+ ### Transformations
31
+
32
+ The tool fetches SVG icons directly from the Iconify API with width and height attributes removed automatically. You can optionally provide a transform file using either JavaScript or TypeScript containing custom transformations for more advanced modifications.
33
+
34
+ #### TypeScript Transform Example
35
+
36
+ ```ts
37
+ // my-transform.ts
38
+ import type { TransformArgs } from '@wkovacs64/add-icon';
39
+
40
+ /**
41
+ * Custom transform to add a title element to SVG
42
+ * @param args - Transform arguments containing SVG content and icon information
43
+ * @returns The transformed SVG
44
+ */
45
+ export default function addTitle(args: TransformArgs): string {
46
+ const titleElement = `<title>${args.iconSet}:${args.iconName}</title>`;
47
+ return args.svg.replace(/<svg([^>]*)>/, `<svg$1>${titleElement}`);
48
+ }
49
+ ```
50
+
51
+ Then use it with the CLI:
52
+
53
+ ```bash
54
+ npx @wkovacs64/add-icon heroicons:arrow-up-circle --transform ./my-transform.ts
55
+ ```
56
+
57
+ ### Configuration File
58
+
59
+ You can create a configuration file in your project root, using either JavaScript (`add-icon.config.js`) or TypeScript (`add-icon.config.ts`).
60
+
61
+ #### TypeScript Configuration Example
62
+
63
+ ```ts
64
+ import type { Config, TransformArgs, TransformFunction } from '@wkovacs64/add-icon';
65
+
66
+ // Define custom transform
67
+ function addCustomAttribute(args: TransformArgs): string {
68
+ return args.svg.replace(/<svg/, `<svg data-icon="${args.iconName}"`);
69
+ }
70
+
71
+ const config = {
72
+ outputDir: './assets/icons',
73
+ transforms: [addCustomAttribute],
74
+ } satisfies Config;
75
+
76
+ export default config;
77
+ ```
78
+
79
+ ## License
80
+
81
+ MIT
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+
3
+ import url from 'node:url';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import { runCli } from '../dist/index.js';
7
+
8
+ const __filename = url.fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ // This logic ensures we only run the CLI when this file is called directly
12
+ // and not when it's imported as a module
13
+ if (
14
+ os.platform() === 'win32'
15
+ ? process.argv[1] === __filename
16
+ : process.argv[1] === __filename || process.argv[1] === __dirname
17
+ ) {
18
+ runCli();
19
+ }
@@ -0,0 +1,11 @@
1
+ import type { Config } from './types.js';
2
+ /**
3
+ * Default configuration
4
+ */
5
+ export declare const defaultConfig: Config;
6
+ /**
7
+ * Loads configuration from file if it exists
8
+ * @param configPath - Path to config file
9
+ * @returns Configuration object
10
+ */
11
+ export declare function loadConfig(configPath?: string): Promise<Config>;
package/dist/config.js ADDED
@@ -0,0 +1,55 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { importModule } from './import-module.js';
4
+ /**
5
+ * Default configuration
6
+ */
7
+ export const defaultConfig = {
8
+ outputDir: '.', // Current directory
9
+ };
10
+ /**
11
+ * Loads configuration from file if it exists
12
+ * @param configPath - Path to config file
13
+ * @returns Configuration object
14
+ */
15
+ export async function loadConfig(configPath) {
16
+ try {
17
+ // If a specific config path is provided, use it
18
+ if (configPath) {
19
+ // Use the unified import method for both JS and TS files
20
+ const config = await importModule(configPath);
21
+ return { ...defaultConfig, ...(config.default || {}) };
22
+ }
23
+ // Try to find a config file in the current directory, checking both JS and TS
24
+ const jsConfigPath = path.resolve(process.cwd(), 'add-icon.config.js');
25
+ const tsConfigPath = path.resolve(process.cwd(), 'add-icon.config.ts');
26
+ // Check for TypeScript config first
27
+ if (existsSync(tsConfigPath)) {
28
+ try {
29
+ const config = await importModule(tsConfigPath);
30
+ return { ...defaultConfig, ...(config.default || {}) };
31
+ }
32
+ catch (err) {
33
+ console.error('Error loading TypeScript config, falling back to default config:', err);
34
+ return defaultConfig;
35
+ }
36
+ }
37
+ // Then check for JavaScript config
38
+ if (existsSync(jsConfigPath)) {
39
+ try {
40
+ const config = await importModule(jsConfigPath);
41
+ return { ...defaultConfig, ...(config.default || {}) };
42
+ }
43
+ catch (err) {
44
+ console.error('Error loading JavaScript config, falling back to default config:', err);
45
+ return defaultConfig;
46
+ }
47
+ }
48
+ // Fall back to default config
49
+ return defaultConfig;
50
+ }
51
+ catch (error) {
52
+ console.error('Error loading config, using default config:', error);
53
+ return defaultConfig;
54
+ }
55
+ }
@@ -0,0 +1,17 @@
1
+ import type { Config } from './types.js';
2
+ /**
3
+ * Parses an icon reference into iconSet and iconName
4
+ * @param iconReference - Reference in format 'iconSet:iconName'
5
+ * @returns Object with iconSet and iconName
6
+ */
7
+ export declare function parseIconReference(iconReference: string): {
8
+ iconSet: string;
9
+ iconName: string;
10
+ };
11
+ /**
12
+ * Downloads an icon and applies transforms
13
+ * @param iconReference - Icon reference (e.g., 'heroicons:arrow-up-circle')
14
+ * @param config - Configuration options
15
+ * @returns Path to saved icon file
16
+ */
17
+ export declare function downloadIcon(iconReference: string, config: Config): Promise<string>;
@@ -0,0 +1,80 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /**
4
+ * Parses an icon reference into iconSet and iconName
5
+ * @param iconReference - Reference in format 'iconSet:iconName'
6
+ * @returns Object with iconSet and iconName
7
+ */
8
+ export function parseIconReference(iconReference) {
9
+ const parts = iconReference.split(':');
10
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
11
+ throw new Error(`Invalid icon reference: ${iconReference}. Expected format: iconSet:iconName`);
12
+ }
13
+ return {
14
+ iconSet: parts[0],
15
+ iconName: parts[1],
16
+ };
17
+ }
18
+ /**
19
+ * Fetches icon SVG directly from Iconify API
20
+ * @param iconSet - Icon set name
21
+ * @param iconName - Icon name
22
+ * @returns Promise with SVG string
23
+ */
24
+ async function fetchIconSvg(iconSet, iconName) {
25
+ // Using width=unset parameter to remove width/height attributes automatically
26
+ const apiUrl = `https://api.iconify.design/${iconSet}/${iconName}.svg?width=unset`;
27
+ try {
28
+ const response = await fetch(apiUrl);
29
+ if (!response.ok) {
30
+ throw new Error(`HTTP error! Status: ${response.status}`);
31
+ }
32
+ return await response.text();
33
+ }
34
+ catch (error) {
35
+ const errorMessage = error instanceof Error ? error.message : String(error);
36
+ throw new Error(`Failed to fetch icon SVG: ${errorMessage}`);
37
+ }
38
+ }
39
+ /**
40
+ * Downloads an icon and applies transforms
41
+ * @param iconReference - Icon reference (e.g., 'heroicons:arrow-up-circle')
42
+ * @param config - Configuration options
43
+ * @returns Path to saved icon file
44
+ */
45
+ export async function downloadIcon(iconReference, config) {
46
+ try {
47
+ const { iconSet, iconName } = parseIconReference(iconReference);
48
+ // Use default output directory if not specified
49
+ const outputDir = config.outputDir || '.';
50
+ // Ensure the output directory exists
51
+ if (!fs.existsSync(outputDir)) {
52
+ fs.mkdirSync(outputDir, { recursive: true });
53
+ }
54
+ // Fetch SVG directly with width=unset parameter to remove width/height attributes
55
+ let svg = await fetchIconSvg(iconSet, iconName);
56
+ // Apply custom transforms if specified
57
+ if (config.transforms && config.transforms.length > 0) {
58
+ for (const transform of config.transforms) {
59
+ // Create transform arguments object
60
+ const transformArgs = {
61
+ svg,
62
+ iconSet,
63
+ iconName,
64
+ };
65
+ // Apply transform
66
+ svg = await Promise.resolve(transform(transformArgs));
67
+ }
68
+ }
69
+ // Create file name
70
+ const fileName = `${iconSet}-${iconName}.svg`;
71
+ const filePath = path.join(outputDir, fileName);
72
+ // Write the SVG file
73
+ fs.writeFileSync(filePath, svg, 'utf8');
74
+ return filePath;
75
+ }
76
+ catch (error) {
77
+ const errorMessage = error instanceof Error ? error.message : String(error);
78
+ throw new Error(`Failed to download icon: ${errorMessage}`);
79
+ }
80
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Imports a module file (JavaScript or TypeScript) by processing it with esbuild
3
+ * @param filePath - Path to module file (JS or TS)
4
+ * @returns Module exports
5
+ */
6
+ export declare function importModule(filePath: string): Promise<any>;
@@ -0,0 +1,35 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import * as esbuild from 'esbuild';
4
+ /**
5
+ * Imports a module file (JavaScript or TypeScript) by processing it with esbuild
6
+ * @param filePath - Path to module file (JS or TS)
7
+ * @returns Module exports
8
+ */
9
+ export async function importModule(filePath) {
10
+ const absolutePath = path.resolve(filePath);
11
+ try {
12
+ // Read the module file content
13
+ const code = await fs.readFile(absolutePath, "utf-8");
14
+ // Determine the appropriate loader based on file extension
15
+ const loader = absolutePath.endsWith('.ts') ? 'ts' : 'js';
16
+ // Use esbuild to transform the code to ESM JS
17
+ const result = await esbuild.transform(code, {
18
+ loader, // Automatically use the appropriate loader
19
+ format: "esm", // Output format
20
+ sourcemap: false, // Disable source maps for data URI
21
+ sourcefile: absolutePath, // Helps with error messages
22
+ target: 'esnext',
23
+ });
24
+ const jsCode = result.code;
25
+ // Create data URI and import
26
+ const base64Code = Buffer.from(jsCode).toString("base64");
27
+ const dataUri = `data:text/javascript;base64,${base64Code}`;
28
+ // Import the transformed code as a module
29
+ return await import(dataUri);
30
+ }
31
+ catch (error) {
32
+ console.error(`Error importing module ${filePath} with esbuild:`, error);
33
+ throw error;
34
+ }
35
+ }
@@ -0,0 +1,5 @@
1
+ import type { TransformFunction, TransformArgs, Config } from './types.js';
2
+ export type { TransformFunction, TransformArgs, Config };
3
+ export { downloadIcon, parseIconReference } from './iconify.js';
4
+ export { loadConfig, defaultConfig } from './config.js';
5
+ export declare function runCli(): void;
package/dist/index.js ADDED
@@ -0,0 +1,76 @@
1
+ import path from 'node:path';
2
+ import { Command } from 'commander';
3
+ import { downloadIcon } from './iconify.js';
4
+ import { loadConfig } from './config.js';
5
+ import { importModule } from './import-module.js';
6
+ import { getPackageInfo } from './package-info.js';
7
+ // Re-export other useful functions
8
+ export { downloadIcon, parseIconReference } from './iconify.js';
9
+ export { loadConfig, defaultConfig } from './config.js';
10
+ // Create CLI program
11
+ const program = new Command();
12
+ // Set up the program with package info
13
+ const setupProgram = async () => {
14
+ const { name, version, description } = await getPackageInfo();
15
+ return program
16
+ .name(name.split('/').pop() || name)
17
+ .description(description)
18
+ .version(version, '-v, --version', 'Output the current version')
19
+ .argument('<icon>', 'Icon reference (e.g., heroicons:arrow-up-circle)')
20
+ .option('-o, --output-dir <dir>', 'Directory to save icon')
21
+ .option('-c, --config <path>', 'Path to config file')
22
+ .option('-t, --transform <path>', 'Path to custom transform module (.js or .ts)');
23
+ };
24
+ // Initialize the program
25
+ const initializedProgram = await setupProgram();
26
+ initializedProgram.action(async (icon, options) => {
27
+ try {
28
+ // Load config (first from config file, then override with CLI options)
29
+ const config = await loadConfig(options.config);
30
+ // Override output directory if specified in CLI
31
+ if (options.outputDir) {
32
+ config.outputDir = options.outputDir;
33
+ }
34
+ // Load custom transform if specified
35
+ if (options.transform) {
36
+ try {
37
+ const transformPath = path.resolve(process.cwd(), options.transform);
38
+ let customTransform;
39
+ try {
40
+ // Use unified import method for both JS and TS files
41
+ customTransform = await importModule(transformPath);
42
+ }
43
+ catch (err) {
44
+ const errorMessage = err instanceof Error ? err.message : String(err);
45
+ console.error(`Error loading transform: ${errorMessage}`);
46
+ process.exit(1);
47
+ }
48
+ if (customTransform && typeof customTransform.default === 'function') {
49
+ config.transforms = [customTransform.default];
50
+ }
51
+ else {
52
+ console.error('Custom transform must export a default function');
53
+ process.exit(1);
54
+ }
55
+ }
56
+ catch (error) {
57
+ const errorMessage = error instanceof Error ? error.message : String(error);
58
+ console.error(`Failed to load custom transform: ${errorMessage}`);
59
+ process.exit(1);
60
+ }
61
+ }
62
+ // Download the icon
63
+ console.log(`Downloading icon: ${icon}...`);
64
+ const savedPath = await downloadIcon(icon, config);
65
+ console.log(`✓ Icon saved to: ${savedPath}`);
66
+ }
67
+ catch (error) {
68
+ const errorMessage = error instanceof Error ? error.message : String(error);
69
+ console.error(`Error: ${errorMessage}`);
70
+ process.exit(1);
71
+ }
72
+ });
73
+ // Parse command line arguments if called directly
74
+ export function runCli() {
75
+ program.parse(process.argv);
76
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Loads package info from package.json
3
+ * @returns Package info with name, version, and description
4
+ */
5
+ export declare function getPackageInfo(): Promise<{
6
+ name: string;
7
+ version: string;
8
+ description: string;
9
+ }>;
@@ -0,0 +1,34 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs/promises';
4
+ /**
5
+ * Loads package info from package.json
6
+ * @returns Package info with name, version, and description
7
+ */
8
+ export async function getPackageInfo() {
9
+ // Get the directory of the current module
10
+ const currentFileUrl = import.meta.url;
11
+ const currentFilePath = fileURLToPath(currentFileUrl);
12
+ const currentDir = path.dirname(currentFilePath);
13
+ // Go up one level from src/ to the package root
14
+ const packageJsonPath = path.resolve(currentDir, '..', 'package.json');
15
+ try {
16
+ // Read and parse package.json
17
+ const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
18
+ const packageJson = JSON.parse(packageJsonContent);
19
+ return {
20
+ name: packageJson.name || 'unknown',
21
+ version: packageJson.version || '0.0.0',
22
+ description: packageJson.description || 'unknown',
23
+ };
24
+ }
25
+ catch (error) {
26
+ console.warn('Failed to read package.json:', error);
27
+ // Fallback values
28
+ return {
29
+ name: 'unknown',
30
+ version: '0.0.0',
31
+ description: 'unknown',
32
+ };
33
+ }
34
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Arguments passed to transform functions
3
+ */
4
+ export type TransformArgs = {
5
+ /** The SVG content as a string */
6
+ svg: string;
7
+ /** The icon set (e.g., 'heroicons') */
8
+ iconSet: string;
9
+ /** The icon name (e.g., 'arrow-up-circle') */
10
+ iconName: string;
11
+ };
12
+ /**
13
+ * SVG transformation function type
14
+ */
15
+ export type TransformFunction = (args: TransformArgs) => Promise<string> | string;
16
+ /**
17
+ * Configuration options for the add-icon CLI
18
+ */
19
+ export type Config = {
20
+ /** Directory to output icons */
21
+ outputDir?: string;
22
+ /** Array of transform functions to apply to icons */
23
+ transforms?: TransformFunction[];
24
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@wkovacs64/add-icon",
3
+ "version": "0.1.0-dev.14f260a6",
4
+ "description": "CLI tool to download and transform icons from Iconify",
5
+ "keywords": [
6
+ "iconify",
7
+ "icons",
8
+ "svg",
9
+ "cli",
10
+ "download"
11
+ ],
12
+ "author": "Justin R. Hall <justin.r.hall@gmail.com>",
13
+ "license": "MIT",
14
+ "type": "module",
15
+ "main": "dist/index.js",
16
+ "exports": {
17
+ ".": "./dist/index.js",
18
+ "./package.json": "./package.json"
19
+ },
20
+ "bin": {
21
+ "add-icon": "bin/add-icon.js"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "bin"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "clean": "del-cli dist",
30
+ "prebuild": "npm run --silent clean",
31
+ "prepublishOnly": "run-p --silent lint typecheck build",
32
+ "start": "node dist/index.js",
33
+ "dev": "tsx src/index.ts",
34
+ "typecheck": "attw --pack --profile esm-only",
35
+ "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
36
+ "format": "prettier --cache --write .",
37
+ "format:check": "prettier --cache --check .",
38
+ "changeset": "changeset",
39
+ "changeset:version": "changeset version && npm install --package-lock-only",
40
+ "changeset:publish": "changeset publish"
41
+ },
42
+ "prettier": "@wkovacs64/prettier-config",
43
+ "engines": {
44
+ "node": ">=20.19.0"
45
+ },
46
+ "dependencies": {
47
+ "commander": "^13.1.0",
48
+ "esbuild": "~0.25.2",
49
+ "typescript": "^5.8.3"
50
+ },
51
+ "devDependencies": {
52
+ "@arethetypeswrong/cli": "0.17.4",
53
+ "@changesets/changelog-github": "0.5.1",
54
+ "@changesets/cli": "2.29.0",
55
+ "@types/node": "22.14.1",
56
+ "@wkovacs64/eslint-config": "7.5.2",
57
+ "@wkovacs64/prettier-config": "4.1.1",
58
+ "del-cli": "6.0.0",
59
+ "eslint": "9.24.0",
60
+ "npm-run-all2": "7.0.2",
61
+ "prettier": "3.5.3",
62
+ "tsx": "4.19.3"
63
+ }
64
+ }