@cocoar/icons-cli 0.1.0-beta.34

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.
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @cocoar/icons-cli
5
+ *
6
+ * Tools for preparing SVG icons for use with @cocoar/vue-ui.
7
+ * Works with any directory of SVG files (Lucide, custom icons, etc.).
8
+ *
9
+ * Commands:
10
+ * normalize Validate, normalize SVGs and generate a manifest
11
+ * bundle Generate a TypeScript icon map from SVGs
12
+ */
13
+
14
+ import { normalize } from '../lib/normalize.mjs';
15
+ import { bundle } from '../lib/bundle.mjs';
16
+
17
+ const USAGE = `
18
+ Usage: cocoar-icons <command> [options]
19
+
20
+ Commands:
21
+ normalize --from <dir> --output <dir> Validate, normalize SVGs and generate manifest
22
+ bundle --from <dir> [--icons <name1,...>] --output <file> Generate a TypeScript icon map
23
+
24
+ Examples:
25
+ cocoar-icons normalize --from ./my-svgs --output ./public/icons
26
+ cocoar-icons bundle --from ./my-svgs --output ./src/my-icons.ts
27
+ cocoar-icons bundle --from ./node_modules/lucide-static/icons --icons "calendar,star" --output ./src/extra-icons.ts
28
+ `.trim();
29
+
30
+ const command = process.argv[2];
31
+
32
+ switch (command) {
33
+ case 'normalize':
34
+ normalize(process.argv.slice(3));
35
+ break;
36
+ case 'bundle':
37
+ bundle(process.argv.slice(3));
38
+ break;
39
+ default:
40
+ console.error(USAGE);
41
+ process.exit(command ? 1 : 0);
42
+ }
package/lib/bundle.mjs ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Bundle SVG Icons
3
+ *
4
+ * Generates a TypeScript icon map from SVG files in a directory.
5
+ * The output file can be registered as an additional CoarIconMapSource.
6
+ * Works with any SVG source (Lucide, custom icons, etc.).
7
+ */
8
+
9
+ import { readdir, readFile, writeFile } from 'fs/promises';
10
+ import { join, parse, resolve } from 'path';
11
+ import { normalizeSvg, validateSvg, escapeSvgForJs } from './svg-utils.mjs';
12
+
13
+ function parseArgs(args) {
14
+ let from = null;
15
+ let icons = null;
16
+ let output = null;
17
+
18
+ for (let i = 0; i < args.length; i++) {
19
+ if (args[i] === '--from' && i + 1 < args.length) {
20
+ from = args[i + 1];
21
+ } else if (args[i] === '--icons' && i + 1 < args.length) {
22
+ icons = args[i + 1];
23
+ } else if (args[i] === '--output' && i + 1 < args.length) {
24
+ output = args[i + 1];
25
+ }
26
+ }
27
+
28
+ return { from, icons, output };
29
+ }
30
+
31
+ export async function bundle(args) {
32
+ const { from: fromArg, icons: iconsArg, output: outputArg } = parseArgs(args);
33
+
34
+ if (!fromArg || !outputArg) {
35
+ console.error(
36
+ 'Usage: cocoar-icons bundle --from <svg-directory> [--icons <name1,name2,...>] --output <file.ts>',
37
+ );
38
+ console.error('');
39
+ console.error('Examples:');
40
+ console.error(
41
+ ' cocoar-icons bundle --from ./my-svgs --output ./src/my-icons.ts',
42
+ );
43
+ console.error(
44
+ ' cocoar-icons bundle --from ./node_modules/lucide-static/icons --icons "calendar,star" --output ./src/extra-icons.ts',
45
+ );
46
+ process.exit(1);
47
+ }
48
+
49
+ const sourceDir = resolve(process.cwd(), fromArg);
50
+ const outputFile = resolve(process.cwd(), outputArg);
51
+
52
+ // Determine which icons to include
53
+ let iconNames;
54
+ if (iconsArg) {
55
+ iconNames = iconsArg
56
+ .split(',')
57
+ .map((n) => n.trim())
58
+ .filter(Boolean);
59
+ if (iconNames.length === 0) {
60
+ console.error('❌ No icon names provided');
61
+ process.exit(1);
62
+ }
63
+ } else {
64
+ // No --icons filter: bundle all SVGs in the directory
65
+ const files = await readdir(sourceDir);
66
+ iconNames = files
67
+ .filter((f) => f.endsWith('.svg'))
68
+ .map((f) => parse(f).name)
69
+ .sort();
70
+ if (iconNames.length === 0) {
71
+ console.error(`❌ No SVG files found in ${sourceDir}`);
72
+ process.exit(1);
73
+ }
74
+ }
75
+
76
+ console.warn('📦 Bundling icons...');
77
+ console.warn(`📂 Source: ${sourceDir}`);
78
+ console.warn(`📋 Icons: ${iconNames.length}`);
79
+ console.warn(`📝 Output: ${outputFile}`);
80
+
81
+ const iconEntries = [];
82
+ const notFound = [];
83
+
84
+ for (const name of iconNames) {
85
+ let content;
86
+ try {
87
+ content = await readFile(join(sourceDir, `${name}.svg`), 'utf-8');
88
+ } catch {
89
+ notFound.push(name);
90
+ continue;
91
+ }
92
+
93
+ validateSvg(content, `${name}.svg`);
94
+ const normalized = normalizeSvg(content);
95
+ const escaped = escapeSvgForJs(normalized);
96
+
97
+ iconEntries.push(` '${name}': \`${escaped}\`,`);
98
+ console.warn(` ✓ ${name}`);
99
+ }
100
+
101
+ if (notFound.length > 0) {
102
+ console.error(`❌ Icons not found: ${notFound.join(', ')}`);
103
+ process.exit(1);
104
+ }
105
+
106
+ const output = `/**
107
+ * Icon bundle.
108
+ *
109
+ * ⚠️ DO NOT EDIT THIS FILE MANUALLY
110
+ *
111
+ * Auto-generated by: cocoar-icons bundle
112
+ * Source: ${fromArg}
113
+ * Icons: ${iconEntries.length}
114
+ *
115
+ * To regenerate:
116
+ * npx @cocoar/icons bundle --from "${fromArg}"${iconsArg ? ` --icons "${iconNames.join(',')}"` : ''} --output ${outputArg}
117
+ */
118
+
119
+ export const CUSTOM_ICONS: Record<string, string> = {
120
+ ${iconEntries.join('\n')}
121
+ };
122
+ `;
123
+
124
+ await writeFile(outputFile, output, 'utf-8');
125
+
126
+ console.warn(`✅ Generated ${outputFile} with ${iconEntries.length} icons`);
127
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Normalize SVG Icons
3
+ *
4
+ * Validates and normalizes SVG icons from a source directory,
5
+ * writes cleaned SVGs to an output directory, and generates
6
+ * a _manifest.json with all icon names.
7
+ *
8
+ * Useful for preparing icons for HTTP serving with CoarHttpIconSource.
9
+ */
10
+
11
+ import { readdir, readFile, mkdir, writeFile } from 'fs/promises';
12
+ import { join, parse, resolve } from 'path';
13
+ import { normalizeSvg, validateSvg } from './svg-utils.mjs';
14
+
15
+ function parseArgs(args) {
16
+ let from = null;
17
+ let output = null;
18
+
19
+ for (let i = 0; i < args.length; i++) {
20
+ if (args[i] === '--from' && i + 1 < args.length) {
21
+ from = args[i + 1];
22
+ } else if (args[i] === '--output' && i + 1 < args.length) {
23
+ output = args[i + 1];
24
+ }
25
+ }
26
+
27
+ return { from, output };
28
+ }
29
+
30
+ export async function normalize(args) {
31
+ const { from: fromArg, output: outputArg } = parseArgs(args);
32
+
33
+ if (!fromArg || !outputArg) {
34
+ console.error('Usage: cocoar-icons normalize --from <svg-directory> --output <directory>');
35
+ console.error('');
36
+ console.error('Examples:');
37
+ console.error(' cocoar-icons normalize --from ./my-svgs --output ./public/icons');
38
+ console.error(' cocoar-icons normalize --from ./node_modules/lucide-static/icons --output ./public/icons/lucide');
39
+ process.exit(1);
40
+ }
41
+
42
+ const sourceDir = resolve(process.cwd(), fromArg);
43
+ const outputDir = resolve(process.cwd(), outputArg);
44
+
45
+ console.warn('📦 Normalizing icons...');
46
+ console.warn(`📂 Source: ${sourceDir}`);
47
+ console.warn(`📝 Output: ${outputDir}`);
48
+
49
+ const files = await readdir(sourceDir);
50
+ const svgFiles = files.filter((f) => f.endsWith('.svg')).sort();
51
+
52
+ if (svgFiles.length === 0) {
53
+ console.error(`❌ No SVG files found in ${sourceDir}`);
54
+ process.exit(1);
55
+ }
56
+
57
+ console.warn(`📁 Found ${svgFiles.length} SVG icons`);
58
+
59
+ await mkdir(outputDir, { recursive: true });
60
+
61
+ let copied = 0;
62
+ let skipped = 0;
63
+ const copiedNames = [];
64
+
65
+ for (const file of svgFiles) {
66
+ const content = await readFile(join(sourceDir, file), 'utf-8');
67
+
68
+ try {
69
+ validateSvg(content, file);
70
+ } catch {
71
+ console.warn(` ⚠️ Skipped ${file} (failed validation)`);
72
+ skipped++;
73
+ continue;
74
+ }
75
+
76
+ const normalized = normalizeSvg(content);
77
+ await writeFile(join(outputDir, file), normalized, 'utf-8');
78
+ copiedNames.push(parse(file).name);
79
+ copied++;
80
+ }
81
+
82
+ console.warn(`✅ Copied ${copied} icons to ${outputDir}`);
83
+ if (skipped > 0) {
84
+ console.warn(`⚠️ Skipped ${skipped} icons (failed validation)`);
85
+ }
86
+
87
+ await writeFile(
88
+ join(outputDir, '_manifest.json'),
89
+ JSON.stringify(copiedNames, null, 2),
90
+ 'utf-8',
91
+ );
92
+ console.warn(`📋 Wrote _manifest.json with ${copiedNames.length} icon names`);
93
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * SVG Utilities for icon processing.
3
+ */
4
+
5
+ /**
6
+ * Normalize SVG content for production use:
7
+ * - Remove XML declaration
8
+ * - Remove comments
9
+ * - Trim whitespace
10
+ * - Minify (remove newlines between tags)
11
+ */
12
+ export function normalizeSvg(svg) {
13
+ return svg
14
+ .replace(/<\?xml[^>]*\?>/g, '')
15
+ .replace(/<!--[\s\S]*?-->/g, '')
16
+ .trim()
17
+ .replace(/>\s+</g, '><')
18
+ .replace(/\s+/g, ' ');
19
+ }
20
+
21
+ /**
22
+ * Validate SVG content for security issues.
23
+ *
24
+ * @param {string} svg - SVG content to validate
25
+ * @param {string} filename - Filename for error messages
26
+ * @throws {Error} If dangerous content found
27
+ */
28
+ export function validateSvg(svg, filename) {
29
+ const dangerous = [
30
+ { pattern: /<script/i, name: 'script tags' },
31
+ { pattern: /on\w+=/i, name: 'event handlers' },
32
+ { pattern: /<foreignObject/i, name: 'foreignObject' },
33
+ { pattern: /@import/i, name: 'CSS imports' },
34
+ { pattern: /javascript:/i, name: 'javascript: URIs' },
35
+ ];
36
+
37
+ for (const { pattern, name } of dangerous) {
38
+ if (pattern.test(svg)) {
39
+ throw new Error(`Dangerous content found in ${filename}: ${name}`);
40
+ }
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Escape SVG content for use in JavaScript template literals.
46
+ */
47
+ export function escapeSvgForJs(svg) {
48
+ return svg
49
+ .replace(/\\/g, '\\\\')
50
+ .replace(/`/g, '\\`')
51
+ .replace(/\$/g, '\\$');
52
+ }
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@cocoar/icons-cli",
3
+ "version": "0.1.0-beta.34",
4
+ "type": "module",
5
+ "description": "CLI tools for managing SVG icons with @cocoar/vue-ui",
6
+ "license": "Apache-2.0",
7
+ "bin": {
8
+ "cocoar-icons": "./bin/cocoar-icons.mjs"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "lib"
13
+ ]
14
+ }