@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.
- package/bin/cocoar-icons.mjs +42 -0
- package/lib/bundle.mjs +127 -0
- package/lib/normalize.mjs +93 -0
- package/lib/svg-utils.mjs +52 -0
- package/package.json +14 -0
|
@@ -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
|
+
}
|