import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, watch, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const isWatch = process.argv.includes('--watch'); import { transform } from '@svgr/core'; import { camelCase, pascalCase } from '@videojs/utils/string'; import { transform as esbuildTransform } from 'esbuild'; import { type Config, optimize } from 'svgo'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = join(__dirname, '..'); const ASSETS_DIR = join(ROOT, 'src/assets'); const DIST_DIR = join(ROOT, 'dist'); const FRAMEWORKS = ['react', 'html'] as const; type Framework = (typeof FRAMEWORKS)[number]; const SVGO_CONFIG: Config = { multipass: true, plugins: [ { name: 'preset-default', params: { overrides: { removeViewBox: false, }, }, }, { name: 'removeAttrs', params: { attrs: ['^fill$', '^stroke$', '^clip-rule$', '^fill-rule$'], }, }, { name: 'addAttributesToSVGElement', params: { attributes: [{ fill: 'currentColor' }], }, }, ], }; function ensureDir(path: string): void { if (!existsSync(path)) mkdirSync(path, { recursive: true }); } function cleanDist(): void { if (existsSync(DIST_DIR)) rmSync(DIST_DIR, { recursive: true, force: true }); } function getIconSets(): string[] { if (!existsSync(ASSETS_DIR)) { console.error(`Assets directory not found: ${ASSETS_DIR}`); process.exit(1); } return readdirSync(ASSETS_DIR).filter((item) => !item.startsWith('.') && item !== 'index'); } function getSvgFiles(setName: string): string[] { return readdirSync(join(ASSETS_DIR, setName)).filter((f) => f.endsWith('.svg')); } function optimizeSvg(svgContent: string): string { return optimize(svgContent, SVGO_CONFIG).data; } async function buildReactComponent(svgContent: string, componentName: string): Promise<{ js: string; tsx: string }> { const transformOpts: Parameters[1] = { plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'], jsxRuntime: 'automatic', svgoConfig: SVGO_CONFIG, }; const tsxCode = await transform(svgContent, { ...transformOpts, typescript: true }, { componentName }); const jsxCode = await transform(svgContent, transformOpts, { componentName }); // SVGR outputs JSX syntax which is invalid in .js files — compile to JS const { code } = await esbuildTransform(jsxCode, { loader: 'jsx', jsx: 'automatic' }); return { js: code, tsx: tsxCode }; } function buildHtmlExport(svgContent: string, varName: string): string { return `export const ${varName} = \`${optimizeSvg(svgContent)}\`;\n`; } function buildIndexExports(icons: { name: string; varName: string }[], framework: Framework): string { return icons .map(({ name, varName }) => framework === 'react' ? `export { default as ${pascalCase(varName)}Icon } from './${name}.js';` : `export { ${camelCase(varName)}Icon } from './${name}.js';` ) .join('\n'); } function buildIndexTypes(icons: { name: string; varName: string }[], framework: Framework): string { const types = icons.map(({ varName }) => framework === 'react' ? `export declare const ${pascalCase(varName)}Icon: React.ForwardRefExoticComponent & React.RefAttributes>;` : `export declare const ${camelCase(varName)}Icon: string;` ); return `/// \n${types.join('\n')}\n`; } async function buildIconSet(setName: string): Promise { const svgFiles = getSvgFiles(setName); console.log(`Building set: ${setName} (${svgFiles.length} icons)`); const icons = svgFiles.map((file) => ({ name: file.replace('.svg', ''), varName: file.replace('.svg', ''), content: readFileSync(join(ASSETS_DIR, setName, file), 'utf8'), })); for (const framework of FRAMEWORKS) { const outDir = join(DIST_DIR, framework, setName); ensureDir(outDir); for (const icon of icons) { const { name, varName, content } = icon; if (framework === 'react') { const componentName = `${pascalCase(varName)}Icon`; const { js, tsx } = await buildReactComponent(content, componentName); writeFileSync(join(outDir, `${name}.js`), js); writeFileSync(join(outDir, `${name}.tsx`), tsx); writeFileSync( join(outDir, `${name}.d.ts`), `import * as React from 'react';\ndeclare const ${componentName}: React.ForwardRefExoticComponent & React.RefAttributes>;\nexport default ${componentName};\n` ); } else { const varNameCamel = camelCase(varName); writeFileSync(join(outDir, `${name}.js`), buildHtmlExport(content, `${varNameCamel}Icon`)); writeFileSync(join(outDir, `${name}.d.ts`), `export declare const ${varNameCamel}Icon: string;\n`); } } writeFileSync(join(outDir, 'index.js'), buildIndexExports(icons, framework)); writeFileSync(join(outDir, 'index.d.ts'), buildIndexTypes(icons, framework)); } } async function build(): Promise { const sets = getIconSets(); console.log(`Found ${sets.length} icon sets: ${sets.join(', ')}\n`); for (const set of sets) { await buildIconSet(set); } } function debounce(fn: () => void, ms: number): () => void { let timer: ReturnType; return () => { clearTimeout(timer); timer = setTimeout(fn, ms); }; } async function main(): Promise { console.log('Building icons...\n'); cleanDist(); await build(); console.log('\nBuild complete!'); if (isWatch) { const rebuild = debounce(() => { console.log('\nRebuilding icons...\n'); build() .then(() => console.log('\nRebuild complete!')) .catch(console.error); }, 200); watch(ASSETS_DIR, { recursive: true }, (_event, filename) => { if (filename?.endsWith('.svg')) { console.log(`Changed: ${filename}`); rebuild(); } }); console.log('\nWatching for changes...'); } } main().catch(console.error);