Skip to content

Instantly share code, notes, and snippets.

@FlatMapIO
Last active June 13, 2023 10:30
Show Gist options
  • Save FlatMapIO/23904f07dc7b6bd10efbf680b6622cad to your computer and use it in GitHub Desktop.
Save FlatMapIO/23904f07dc7b6bd10efbf680b6622cad to your computer and use it in GitHub Desktop.
generate qwik svg icons
import * as fs from 'node:fs'
import * as path from 'node:path'
import { startCase } from 'lodash-es'
import { format } from 'prettier'
import * as Rx from 'rxjs'
import * as svgo from 'svgo'
import config from './iconset'
const formatOpts = require('../prettier.config.cjs')
const typesTs = `import { QwikIntrinsicElements } from '@builder.io/qwik'
type Size = 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | string
export type IconProps = QwikIntrinsicElements["svg"]
export type RectIconProps = Omit<IconProps, 'width' | 'height'> & { size?: Size}
`
const header = `/**
* This file was automatically generated by scripts/gen-icons.ts
**/
import type { IconProps, RectIconProps } from './types'
`
const generateQwikSvgJsx = ({ name, svg }: { name: string; svg: string }) => {
let width = ''
let height = ''
const processed = svgo.optimize(svg, {
// https://github.com/svg/svgo
plugins: [
'removeComments',
'removeDesc',
'removeDoctype',
'removeMetadata',
'removeEditorsNSData',
'removeTitle',
'removeXMLProcInst',
'removeHiddenElems',
'removeEmptyContainers',
{
name: 'rewrite',
fn: (root, config) => {
return {
element: {
enter(node, parentNode) {
if (node.name === 'svg') {
node.attributes['PROPS'] = '%PROPS_HOLDER%'
node.attributes['key'] = '%KEY_HOLDER%'
width = node.attributes['width']
height = node.attributes['height']
if (!width || !height) {
const vbox = node.attributes['viewBox']
if (!vbox)
throw new Error('width, height, viewBox not found')
const [, , vw, vh] = vbox.split(' ')
if (!width) {
width = vw
}
if (!height) {
height = vh
}
}
node.attributes['width'] = '%WIDTH_HOLDER%'
node.attributes['height'] = '%HEIGHT_HOLDER%'
}
},
},
}
},
},
],
}).data
width = width.trim()
height = height.trim()
const withUnit = /^\d+[a-zA-Z]+$/
let replaced = processed
.replaceAll(`PROPS="%PROPS_HOLDER%"`, '{...props}')
.replaceAll(`"%KEY_HOLDER%"`, `{key}`)
if (width === height) {
replaced = replaced
.replaceAll('"%WIDTH_HOLDER%"', `{size}`)
.replaceAll('"%HEIGHT_HOLDER%"', `{size}`)
return `export const ${name} = ({size=${
withUnit.test(width) ? `"${width}"` : width
}, ...props}: RectIconProps, key: string) => {\n return (\n ${replaced})\n}`
} else {
replaced = replaced
.replaceAll(
'"%WIDTH_HOLDER%"',
`{${withUnit.test(width) ? `"${width}"` : width}}`,
)
.replaceAll(
'"%HEIGHT_HOLDER%"',
`{${withUnit.test(height) ? `"${height}"` : height}}`,
)
return `export const ${name} = (props: IconProps, key: string) => {\n return (\n ${replaced})\n}`
}
}
type IconSetEntry = {
name: string
refer: string
prefix: string
baseUrl: string
icons: string[]
}
export type GeneratorConfig = {
outDir: string
entries: IconSetEntry[]
}
async function generateIconSet(entry: IconSetEntry) {
const iconSet = Array.from(new Set(entry.icons).keys())
const result$ = Rx.from(iconSet).pipe(
Rx.tap((it) => console.log(`fetching ${entry.name}/${it}`)),
Rx.mergeMap(
// (it) => Rx.from(fetch(path.join(entry.baseUrl, it + '.svg'))),
(it) =>
Rx.zip([
Rx.of(it),
Rx.from(fetch(path.join(entry.baseUrl, it + '.svg'))).pipe(
Rx.mergeMap((it) => it.text()),
Rx.map((it) => ({ data: it })),
Rx.catchError((it: Error) => Rx.of({ error: it })),
),
]),
100,
),
Rx.map(([name, result]) => ({ name, result })),
Rx.toArray(),
)
const result = await Rx.lastValueFrom(result$)
let code = header
for (let it of result) {
if ('error' in it.result) {
console.log(`${entry.name}/${it.name} fetch failed`)
continue
}
const iconName = entry.prefix + startCase(it.name).replaceAll(' ', '')
code +=
generateQwikSvgJsx({
name: iconName,
svg: it.result.data,
}) + '\n'
}
const formatted: string = format(code, formatOpts)
return formatted
}
async function build(config: GeneratorConfig) {
if (!fs.statSync(config.outDir).isDirectory()) {
fs.mkdirSync(config.outDir)
}
if (!fs.existsSync(path.join(config.outDir, 'types.ts'))) {
fs.writeFileSync(path.join(config.outDir, 'types.ts'), typesTs, 'utf-8')
}
const all = await Promise.all(config.entries.map(generateIconSet))
for (let i = 0; i < all.length; i++) {
const it = config.entries[i]
fs.writeFileSync(`${config.outDir}/${it.name}.tsx`, all[i], 'utf-8')
}
}
build(config)
import { QwikIntrinsicElements } from '@builder.io/qwik'
type Size = 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | number | string
type IconProps = QwikIntrinsicElements['svg']
type RectIconProps = Omit<IconProps, 'width' | 'height'> & { size?: Size }
export const LuChevronUp = (
{ size = 24, ...props }: RectIconProps,
key: string,
) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
fill='none'
stroke='currentColor'
stroke-linecap='round'
stroke-linejoin='round'
stroke-width='2'
{...props}
key={key}
>
<path d='M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z' />
</svg>
)
}
export const LuChevronDown = (
{ size = 24, ...props }: RectIconProps,
key: string,
) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
fill='none'
stroke='currentColor'
stroke-linecap='round'
stroke-linejoin='round'
stroke-width='2'
{...props}
key={key}
>
<circle cx='12' cy='12' r='10' />
<path d='m4.93 4.93 14.14 14.14' />
</svg>
)
}
import { type GeneratorConfig } from './gen-icons'
const config: GeneratorConfig = {
outDir: './src/components/icons/',
entries: [
{
name: 'lucide',
refer: 'https://lucide.dev/icons/',
prefix: 'Lu',
baseUrl: 'https://cdn.jsdelivr.net/npm/lucide-static@latest/icons',
icons: [
'chevron-up',
'chevron-down',
'repeat-2',
'clipboard',
'clipboard-check',
'bookmark',
'check',
'check-check',
'edit-3',
'settings-2',
'sun-medium',
'arrow-down-to-line',
'moon',
'credit-card',
'arrow-right-from-line',
'thumbs-down',
'thumbs-up',
'stop-circle',
'menu',
'more-horizontal',
'alert-triangle',
'help-circle',
'info',
'trash-2',
'loader-2',
'star',
'ban',
'log-out',
'mic',
'x',
'plus'
],
},
{
name: 'logos',
prefix: 'Lo',
refer: 'https://svgporn.com/',
baseUrl: 'https://raw.githubusercontent.com/gilbarbara/logos/main/logos/',
icons: [
'github-icon',
'google-icon',
'google-bard-icon',
'openai',
'openai-icon',
'apple',
'stripe',
'zapier',
'qwik-icon',
'pinecone',
'notion-icon',
'replit',
'replit-icon',
],
},
// {
// name: 'heroicons',
// prefix: 'He',
// refer: 'https://heroicons.com/',
// baseUrl: 'https://cdn.jsdelivr.net/npm/heroicons@latest/24/outline/',
// icons: ['backspace'],
// },
// {
// name: 'carbon',
// prefix: 'Ca',
// refer: 'https://carbondesignsystem.com/guidelines/icons/library/',
// baseUrl: 'https://cdn.jsdelivr.net/npm/@carbon/icons@latest/svg/24/',
// icons: [
// ],
// },
],
}
export default config
{
"scripts": {
"icons.gen": "tsx scripts/gen-icons.ts",
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment