feat: move pack logic to a web worker (#367)

This commit is contained in:
Joaquín Sánchez 2025-05-30 02:09:37 +02:00 committed by GitHub
parent 96d65bb177
commit 21603701a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 822 additions and 465 deletions

View File

@ -34,6 +34,7 @@
"hotkeys-js": "^3.13.9",
"iconify-icon": "^2.3.0",
"prettier": "^3.5.3",
"ultrahtml": "^1.6.0",
"vue": "^3.5.13",
"vue-chemistry": "^0.2.2",
"vue-router": "^4.5.0"

100
pnpm-lock.yaml generated
View File

@ -35,6 +35,9 @@ importers:
prettier:
specifier: ^3.5.3
version: 3.5.3
ultrahtml:
specifier: ^1.6.0
version: 1.6.0
vue:
specifier: ^3.5.13
version: 3.5.13(typescript@5.8.2)
@ -3302,10 +3305,6 @@ packages:
resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==}
engines: {node: '>=0.10.0'}
is-regex@1.1.4:
resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
engines: {node: '>= 0.4'}
is-regex@1.2.1:
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
engines: {node: '>= 0.4'}
@ -3840,10 +3839,6 @@ packages:
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
object-inspect@1.13.2:
resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==}
engines: {node: '>= 0.4'}
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
@ -4243,10 +4238,6 @@ packages:
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safe-regex-test@1.0.3:
resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==}
engines: {node: '>= 0.4'}
safe-regex-test@1.1.0:
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
engines: {node: '>= 0.4'}
@ -4324,10 +4315,6 @@ packages:
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
engines: {node: '>= 0.4'}
side-channel@1.0.6:
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
engines: {node: '>= 0.4'}
side-channel@1.1.0:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'}
@ -4656,6 +4643,9 @@ packages:
ufo@1.5.4:
resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
ultrahtml@1.6.0:
resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==}
unbox-primitive@1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
@ -7174,7 +7164,7 @@ snapshots:
define-properties: 1.2.1
es-abstract: 1.23.3
es-errors: 1.3.0
get-intrinsic: 1.2.4
get-intrinsic: 1.3.0
is-array-buffer: 3.0.4
is-shared-array-buffer: 1.0.3
@ -7841,36 +7831,36 @@ snapshots:
data-view-buffer: 1.0.1
data-view-byte-length: 1.0.1
data-view-byte-offset: 1.0.0
es-define-property: 1.0.0
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.0.0
es-object-atoms: 1.1.1
es-set-tostringtag: 2.0.3
es-to-primitive: 1.2.1
function.prototype.name: 1.1.6
get-intrinsic: 1.2.4
get-intrinsic: 1.3.0
get-symbol-description: 1.0.2
globalthis: 1.0.4
gopd: 1.0.1
gopd: 1.2.0
has-property-descriptors: 1.0.2
has-proto: 1.0.3
has-symbols: 1.0.3
has-symbols: 1.1.0
hasown: 2.0.2
internal-slot: 1.0.7
is-array-buffer: 3.0.4
is-callable: 1.2.7
is-data-view: 1.0.1
is-negative-zero: 2.0.3
is-regex: 1.1.4
is-regex: 1.2.1
is-shared-array-buffer: 1.0.3
is-string: 1.0.7
is-typed-array: 1.1.13
is-weakref: 1.0.2
object-inspect: 1.13.2
object-inspect: 1.13.4
object-keys: 1.1.1
object.assign: 4.1.5
regexp.prototype.flags: 1.5.2
safe-array-concat: 1.1.2
safe-regex-test: 1.0.3
safe-regex-test: 1.1.0
string.prototype.trim: 1.2.9
string.prototype.trimend: 1.0.8
string.prototype.trimstart: 1.0.8
@ -7901,7 +7891,7 @@ snapshots:
es-set-tostringtag@2.0.3:
dependencies:
get-intrinsic: 1.2.4
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.2
@ -8506,7 +8496,7 @@ snapshots:
dependencies:
call-bind: 1.0.7
es-errors: 1.3.0
get-intrinsic: 1.2.4
get-intrinsic: 1.3.0
get-tsconfig@4.10.0:
dependencies:
@ -8743,7 +8733,7 @@ snapshots:
dependencies:
es-errors: 1.3.0
hasown: 2.0.2
side-channel: 1.0.6
side-channel: 1.1.0
ip-address@9.0.5:
dependencies:
@ -8758,7 +8748,7 @@ snapshots:
is-array-buffer@3.0.4:
dependencies:
call-bind: 1.0.7
get-intrinsic: 1.2.4
get-intrinsic: 1.3.0
is-bigint@1.0.4:
dependencies:
@ -8829,11 +8819,6 @@ snapshots:
is-obj@1.0.1: {}
is-regex@1.1.4:
dependencies:
call-bind: 1.0.7
has-tostringtag: 1.0.2
is-regex@1.2.1:
dependencies:
call-bound: 1.0.4
@ -8855,7 +8840,7 @@ snapshots:
is-symbol@1.0.4:
dependencies:
has-symbols: 1.0.3
has-symbols: 1.1.0
is-typed-array@1.1.13:
dependencies:
@ -9542,8 +9527,6 @@ snapshots:
dependencies:
boolbase: 1.0.0
object-inspect@1.13.2: {}
object-inspect@1.13.4: {}
object-is@1.1.6:
@ -9986,20 +9969,14 @@ snapshots:
safe-array-concat@1.1.2:
dependencies:
call-bind: 1.0.7
get-intrinsic: 1.2.4
has-symbols: 1.0.3
get-intrinsic: 1.3.0
has-symbols: 1.1.0
isarray: 2.0.5
safe-buffer@5.1.2: {}
safe-buffer@5.2.1: {}
safe-regex-test@1.0.3:
dependencies:
call-bind: 1.0.7
es-errors: 1.3.0
is-regex: 1.1.4
safe-regex-test@1.1.0:
dependencies:
call-bound: 1.0.4
@ -10098,13 +10075,6 @@ snapshots:
object-inspect: 1.13.4
side-channel-map: 1.0.1
side-channel@1.0.6:
dependencies:
call-bind: 1.0.7
es-errors: 1.3.0
get-intrinsic: 1.2.4
object-inspect: 1.13.2
side-channel@1.1.0:
dependencies:
es-errors: 1.3.0
@ -10229,33 +10199,33 @@ snapshots:
define-properties: 1.2.1
es-abstract: 1.23.3
es-errors: 1.3.0
es-object-atoms: 1.0.0
get-intrinsic: 1.2.4
gopd: 1.0.1
has-symbols: 1.0.3
es-object-atoms: 1.1.1
get-intrinsic: 1.3.0
gopd: 1.2.0
has-symbols: 1.1.0
internal-slot: 1.0.7
regexp.prototype.flags: 1.5.2
set-function-name: 2.0.2
side-channel: 1.0.6
side-channel: 1.1.0
string.prototype.trim@1.2.9:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-abstract: 1.23.3
es-object-atoms: 1.0.0
es-object-atoms: 1.1.1
string.prototype.trimend@1.0.8:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-object-atoms: 1.0.0
es-object-atoms: 1.1.1
string.prototype.trimstart@1.0.8:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-object-atoms: 1.0.0
es-object-atoms: 1.1.1
string_decoder@1.1.1:
dependencies:
@ -10475,7 +10445,7 @@ snapshots:
dependencies:
call-bind: 1.0.7
for-each: 0.3.3
gopd: 1.0.1
gopd: 1.2.0
has-proto: 1.0.3
is-typed-array: 1.1.13
@ -10484,7 +10454,7 @@ snapshots:
available-typed-arrays: 1.0.7
call-bind: 1.0.7
for-each: 0.3.3
gopd: 1.0.1
gopd: 1.2.0
has-proto: 1.0.3
is-typed-array: 1.1.13
@ -10492,7 +10462,7 @@ snapshots:
dependencies:
call-bind: 1.0.7
for-each: 0.3.3
gopd: 1.0.1
gopd: 1.2.0
has-proto: 1.0.3
is-typed-array: 1.1.13
possible-typed-array-names: 1.0.0
@ -10501,11 +10471,13 @@ snapshots:
ufo@1.5.4: {}
ultrahtml@1.6.0: {}
unbox-primitive@1.0.2:
dependencies:
call-bind: 1.0.7
has-bigints: 1.0.2
has-symbols: 1.0.3
has-symbols: 1.1.0
which-boxed-primitive: 1.0.2
unconfig@7.3.1:

View File

@ -32,6 +32,7 @@ async function packIconFont() {
progressMessage.value = 'Packing up...'
await nextTick()
await PackIconFont(
[props.collection],
props.collection.icons.map(i => `${props.collection!.id}:${i}`),
{ fontName: props.collection.name, fileName: props.collection.id },
)
@ -49,6 +50,7 @@ async function packSvgs() {
progressMessage.value = 'Packing up...'
await nextTick()
await PackSvgZip(
[props.collection],
props.collection.icons.map(i => `${props.collection!.id}:${i}`),
props.collection.id,
)
@ -66,6 +68,7 @@ async function packJson() {
progressMessage.value = 'Packing up...'
await nextTick()
await PackJsonZip(
[props.collection],
props.collection.icons.map(i => `${props.collection!.id}:${i}`),
props.collection.id,
)

View File

@ -1,5 +1,6 @@
<script setup lang='ts'>
import type { PackType } from '../utils/pack'
import type { PackType } from '../utils/svg'
import { collections } from '../data'
import { bags, clearBag } from '../store'
import { PackIconFont, PackSVGSprite, PackZip } from '../utils/pack'
@ -21,18 +22,21 @@ function clear() {
async function packIconFont() {
// TODO: customzie
await PackIconFont(
collections,
bags.value,
)
}
async function packSVGSprite() {
await PackSVGSprite(
collections,
bags.value,
)
}
async function PackSvgs(type: PackType = 'svg') {
await PackZip(
collections,
bags.value,
'icones-bags',
type,

View File

@ -3,8 +3,7 @@ import { collections } from '../data'
import { activeMode, copyPreviewColor, getTransformedId, inBag, preferredCase, previewColor, pushRecentIcon, showCaseSelect, showHelp, toggleBag } from '../store'
import { idCases } from '../utils/case'
import { dataUrlToBlob } from '../utils/dataUrlToBlob'
import { getIconSnippet, SnippetMap, toComponentName } from '../utils/icons'
import { Download } from '../utils/pack'
import { Download, getIconSnippet, SnippetMap, toComponentName } from '../utils/icons'
import InstallIconSet from './InstallIconSet.vue'
const props = defineProps({
@ -70,7 +69,7 @@ async function copyPng(dataUrl: string): Promise<boolean> {
async function copy(type: string) {
pushRecentIcon(props.icon)
const svg = await getIconSnippet(props.icon, type, true, color.value)
const svg = await getIconSnippet(collections, props.icon, type, true, color.value)
if (!svg)
return
@ -84,7 +83,7 @@ async function copy(type: string) {
async function download(type: string) {
pushRecentIcon(props.icon)
const text = await getIconSnippet(props.icon, type, false, color.value)
const text = await getIconSnippet(collections, props.icon, type, false, color.value)
if (!text)
return
const ext = (type === 'solid' || type === 'qwik' || type === 'react-native') ? 'tsx' : type
@ -212,6 +211,7 @@ const collection = computed(() => {
<div class="flex gap-1">
<template v-for="(snippet, type) in group" :key="`${icon}-${groupName}-${type}`">
<SnippetPreview
:collection="collection"
:icon="icon"
:snippet="snippet"
:type="type"

View File

@ -70,7 +70,12 @@ async function onSelect(icon: string) {
toggleBag(icon)
break
case 'copy':
onCopy(await copyText(await getIconSnippet(icon, 'id', true) || icon))
onCopy(await copyText(await getIconSnippet(
[collection.value!],
icon,
'id',
true,
) || icon))
break
default:
current.value = icon

View File

@ -1,11 +1,14 @@
<script lang='ts' setup>
import type { CollectionInfo } from '../data'
import type { Snippet } from '../utils/icons'
import { Menu } from 'floating-vue'
import { collections } from '../data'
import { getIconSnippet } from '../utils/icons'
import { prettierCode } from '../utils/prettier'
import { highlight } from '../utils/shiki'
import { prettierCode } from '../utils/svg'
const props = defineProps<{
collection?: CollectionInfo
icon: string
snippet: Snippet
type: string
@ -15,8 +18,15 @@ const props = defineProps<{
const code = ref<string>('')
async function onShow() {
if (!code.value)
code.value = await getIconSnippet(props.icon, props.type, false, props.color) || ''
if (!code.value) {
code.value = await getIconSnippet(
props.collection ? [props.collection] : collections,
props.icon,
props.type,
false,
props.color,
) || ''
}
}
const highlightCode = computedAsync(async () => {

View File

@ -21,7 +21,7 @@ const router = createRouter({
if (!isElectron && PWA) {
// disable local storage cache when there is PWA:
// we need to keep local storage when running dev server without PWA
// to avoid send requests to iconify server api
// to avoid sending requests to iconify server api
disableCache('all')
router.isReady().then(async () => {
const { registerSW } = await import('virtual:pwa-register')

View File

@ -1,11 +1,24 @@
import type { BuiltInParserName as PrettierParser } from 'prettier'
import { encodeSvgForCss } from '@iconify/utils'
import { buildIcon, loadIcon } from 'iconify-icon'
import { collections } from '../data'
import type { CollectionInfo } from '../data'
import { isVSCode } from '../env'
import { getTransformedId } from '../store'
import Base64 from './base64'
import { HtmlToJSX } from './htmlToJsx'
import { prettierCode } from './prettier'
import { getSvgSymbol } from './pack'
import {
API_ENTRY,
bufferToString,
ClearSvg,
getSvg,
SvgToAstro,
SvgToDataURL,
SvgToJSX,
SvgToQwik,
SvgToReactNative,
SvgToSolid,
SvgToSvelte,
SvgToTSX,
SvgToVue,
toComponentName,
} from './svg'
import { svgToPngDataUrl } from './svgToPng'
export interface Snippet {
@ -14,6 +27,27 @@ export interface Snippet {
lang: string // for shiki
prettierParser: PrettierParser // for prettier
}
export { toComponentName }
export async function Download(blob: Blob, name: string) {
if (isVSCode) {
blob.arrayBuffer().then(
buffer => vscode.postMessage({
command: 'download',
name,
text: bufferToString(buffer),
}),
)
}
else {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = name
a.click()
a.remove()
}
}
export const SnippetMap: Record<string, Record<string, Snippet>> = {
Snippets: {
@ -41,230 +75,13 @@ export const SnippetMap: Record<string, Record<string, Snippet>> = {
},
}
const API_ENTRY = 'https://api.iconify.design'
function getLicenseComment(icon: string) {
const [id] = icon.split(':')
const collection = collections.find(i => i.id === id)
if (!collection) {
return ''
}
return `<!-- Icon from ${collection?.name} by ${collection?.author?.name} - ${collection?.license?.url} -->`
}
export async function getSvgLocal(icon: string, size = '1em', color = 'currentColor') {
const data = await loadIcon(icon)
if (!data)
return
const built = buildIcon(data, { height: size })
if (!built)
return
const license = getLicenseComment(icon)
const xlink = built.body.includes('xlink:') ? ' xmlns:xlink="http://www.w3.org/1999/xlink"' : ''
return `<svg xmlns="http://www.w3.org/2000/svg"${xlink} ${Object.entries(built.attributes).map(([k, v]) => `${k}="${v}"`).join(' ')}>${license}${built.body}</svg>`.replaceAll('currentColor', color)
}
export async function getSvg(icon: string, size = '1em', color = 'currentColor') {
return await getSvgLocal(icon, size, color)
|| await fetch(`${API_ENTRY}/${icon}.svg?inline=false&height=${size}&color=${encodeURIComponent(color)}`).then(r => r.text()) || ''
}
export async function getSvgSymbol(icon: string, size = '1em', color = 'currentColor') {
const svgMarkup = await getSvg(icon, size, color)
const symbolElem = document.createElementNS('http://www.w3.org/2000/svg', 'symbol')
const node = document.createElement('div') // Create any old element
node.innerHTML = svgMarkup
// Grab the inner HTML and move into a symbol element
symbolElem.innerHTML = node.querySelector('svg')!.innerHTML
symbolElem.setAttribute('viewBox', node.querySelector('svg')!.getAttribute('viewBox')!)
symbolElem.id = icon.replace(/:/, '-') // Simple slugify for quick symbol lookup
return symbolElem?.outerHTML
}
export function toComponentName(icon: string) {
return icon.split(/[:\-_]/).filter(Boolean).map(s => s[0].toUpperCase() + s.slice(1).toLowerCase()).join('')
}
export function ClearSvg(svgCode: string, reactJSX?: boolean) {
const el = document.createElement('div')
el.innerHTML = svgCode
const svg = el.getElementsByTagName('svg')[0]
const keep = ['viewBox', 'width', 'height', 'focusable', 'xmlns', 'xlink']
for (const key of Object.values(svg.attributes)) {
if (keep.includes(key.localName))
continue
svg.removeAttributeNode(key)
}
return HtmlToJSX(el.innerHTML, reactJSX)
}
export function SvgToJSX(svg: string, name: string, snippet: boolean) {
const code = `
export function ${name}(props) {
return (
${ClearSvg(svg, true).replace(/<svg (.*?)>/, '<svg $1 {...props}>')}
)
}`
if (snippet)
return prettierCode(code, 'babel-ts')
else
return prettierCode(`import React from 'react'\n${code}\nexport default ${name}`, 'babel-ts')
}
export function SvgToTSX(svg: string, name: string, snippet: boolean, reactJSX = true) {
let code = `
export function ${name}(props: SVGProps<SVGSVGElement>) {
return (
${ClearSvg(svg, reactJSX).replace(/<svg (.*?)>/, '<svg $1 {...props}>')}
)
}`
code = snippet ? code : `import React, { SVGProps } from 'react'\n${code}\nexport default ${name}`
return prettierCode(code, 'babel-ts')
}
export function SvgToQwik(svg: string, name: string, snippet: boolean) {
let code = `
export function ${name}(props: QwikIntrinsicElements['svg'], key: string) {
return (
${ClearSvg(svg, false).replace(/<svg (.*?)>/, '<svg $1 {...props} key={key}>')}
)
}`
code = snippet ? code : `import type { QwikIntrinsicElements } from '@builder.io/qwik'\n${code}\nexport default ${name}`
return prettierCode(code, 'babel-ts')
}
export function SvgToVue(svg: string, name: string, isTs?: boolean) {
const content = `
<template>
${ClearSvg(svg)}
</template>
<script>
export default {
name: '${name}'
}
</script>`
const code = isTs ? content.replace('<script>', '<script lang="ts">') : content
return prettierCode(code, 'vue')
}
export function SvgToSolid(svg: string, name: string, snippet: boolean) {
let code = `
export function ${name}(props: JSX.IntrinsicElements['svg']) {
return (
${svg.replace(/<svg (.*?)>/, '<svg $1 {...props}>')}
)
}`
code = snippet ? code : `import type { JSX } from 'solid-js'\n${code}\nexport default ${name}`
return prettierCode(code, 'babel-ts')
}
export function SvgToSvelte(svg: string) {
return `${svg.replace(/<svg (.*?)>/, '<svg $1 {...$$$props}>')}`
}
export function SvgToAstro(svg: string) {
return `
---
const props = Astro.props
---
${svg.replace(/<svg (.*?)>/, '<svg $1 {...props}>')}
`
}
export function SvgToReactNative(svg: string, name: string, snippet: boolean) {
function replaceTags(svg: string, replacements: {
from: string
to: string
}[]): string {
let result = svg
replacements.forEach(({ from, to }) => {
result = result.replace(new RegExp(`<${from}(.*?)>`, 'g'), `<${to}$1>`)
.replace(new RegExp(`</${from}>`, 'g'), `</${to}>`)
})
return result
}
function generateImports(usedComponents: string[]): string {
// Separate Svg from the other components
const svgIndex = usedComponents.indexOf('Svg')
if (svgIndex !== -1)
usedComponents.splice(svgIndex, 1)
// Join all other component names with a comma and wrap them in curly braces
const componentsString = usedComponents.length > 0 ? `{ ${usedComponents.join(', ')} }` : ''
// Return the consolidated import statement, ensuring Svg is imported as a default import
return `import Svg, ${componentsString} from 'react-native-svg';`
}
const replacements: {
from: string
to: string
}[] = [
{ from: 'svg', to: 'Svg' },
{ from: 'path', to: 'Path' },
{ from: 'g', to: 'G' },
{ from: 'circle', to: 'Circle' },
{ from: 'rect', to: 'Rect' },
{ from: 'line', to: 'Line' },
{ from: 'polyline', to: 'Polyline' },
{ from: 'polygon', to: 'Polygon' },
{ from: 'ellipse', to: 'Ellipse' },
{ from: 'text', to: 'Text' },
{ from: 'tspan', to: 'Tspan' },
{ from: 'textPath', to: 'TextPath' },
{ from: 'defs', to: 'Defs' },
{ from: 'use', to: 'Use' },
{ from: 'symbol', to: 'Symbol' },
{ from: 'linearGradient', to: 'LinearGradient' },
{ from: 'radialGradient', to: 'RadialGradient' },
{ from: 'stop', to: 'Stop' },
]
const reactNativeSvgCode = replaceTags(ClearSvg(svg, true), replacements)
.replace(/className=/g, '')
.replace(/href=/g, 'xlinkHref=')
.replace(/clip-path=/g, 'clipPath=')
.replace(/fill-opacity=/g, 'fillOpacity=')
.replace(/stroke-width=/g, 'strokeWidth=')
.replace(/stroke-linecap=/g, 'strokeLinecap=')
.replace(/stroke-linejoin=/g, 'strokeLinejoin=')
.replace(/stroke-miterlimit=/g, 'strokeMiterlimit=')
const svgComponents = replacements.map(({ to }) => to)
const imports = generateImports(svgComponents.filter(component => reactNativeSvgCode.includes(component)))
let code = `
${imports}
export function ${name}(props) {
return (
${reactNativeSvgCode}
)
}`
if (!snippet)
code = `import React from 'react';\n${code}\nexport default ${name}`
return prettierCode(code, 'babel-ts')
}
export function SvgToDataURL(svg: string) {
const base64 = `data:image/svg+xml;base64,${Base64.encode(svg)}`
const plain = `data:image/svg+xml,${encodeSvgForCss(svg)}`
// Return the shorter of the two data URLs
return base64.length < plain.length ? base64 : plain
}
export async function getIconSnippet(icon: string, type: string, snippet = true, color = 'currentColor'): Promise<string | undefined> {
export async function getIconSnippet(
collections: CollectionInfo[],
icon: string,
type: string,
snippet = true,
color = 'currentColor',
): Promise<string | undefined> {
if (!icon)
return
@ -282,33 +99,33 @@ export async function getIconSnippet(icon: string, type: string, snippet = true,
case 'css':
return `background: url('${url}') no-repeat center center / contain;`
case 'svg':
return await getSvg(icon, '32', color)
return await getSvg(collections, icon, '32', color)
case 'png':
return await svgToPngDataUrl(await getSvg(icon, '32', color))
return await svgToPngDataUrl(await getSvg(collections, icon, '32', color))
case 'svg-symbol':
return await getSvgSymbol(icon, '32', color)
return await getSvgSymbol(collections, icon, '32', color)
case 'data_url':
return SvgToDataURL(await getSvg(icon, undefined, color))
return SvgToDataURL(await getSvg(collections, icon, undefined, color))
case 'pure-jsx':
return ClearSvg(await getSvg(icon, undefined, color))
return ClearSvg(await getSvg(collections, icon, undefined, color))
case 'jsx':
return SvgToJSX(await getSvg(icon, undefined, color), toComponentName(icon), snippet)
return SvgToJSX(await getSvg(collections, icon, undefined, color), toComponentName(icon), snippet)
case 'tsx':
return SvgToTSX(await getSvg(icon, undefined, color), toComponentName(icon), snippet)
return SvgToTSX(await getSvg(collections, icon, undefined, color), toComponentName(icon), snippet)
case 'qwik':
return SvgToQwik(await getSvg(icon, undefined, color), toComponentName(icon), snippet)
return SvgToQwik(await getSvg(collections, icon, undefined, color), toComponentName(icon), snippet)
case 'vue':
return SvgToVue(await getSvg(icon, undefined, color), toComponentName(icon))
return SvgToVue(await getSvg(collections, icon, undefined, color), toComponentName(icon))
case 'vue-ts':
return SvgToVue(await getSvg(icon, undefined, color), toComponentName(icon), true)
return SvgToVue(await getSvg(collections, icon, undefined, color), toComponentName(icon), true)
case 'solid':
return SvgToSolid(await getSvg(icon, undefined, color), toComponentName(icon), snippet)
return SvgToSolid(await getSvg(collections, icon, undefined, color), toComponentName(icon), snippet)
case 'svelte':
return SvgToSvelte(await getSvg(icon, undefined, color))
return SvgToSvelte(await getSvg(collections, icon, undefined, color))
case 'astro':
return SvgToAstro(await getSvg(icon, undefined, color))
return SvgToAstro(await getSvg(collections, icon, undefined, color))
case 'react-native':
return SvgToReactNative(await getSvg(icon, undefined, color), toComponentName(icon), snippet)
return SvgToReactNative(await getSvg(collections, icon, undefined, color), toComponentName(icon), snippet)
case 'unplugin':
return `import ${toComponentName(icon)} from '~icons/${icon.split(':')[0]}/${icon.split(':')[1]}'`
}

View File

@ -0,0 +1,5 @@
import PackerWorker from './worker?worker'
export const packerWorker = new PackerWorker({
name: 'IconesPackWorker',
})

View File

@ -1,79 +1,40 @@
import { isVSCode } from '../env'
import { bufferToString } from './bufferToString'
import {
getSvg,
getSvgSymbol,
SvgToJSX,
SvgToTSX,
SvgToVue,
toComponentName,
} from './icons'
import type { CollectionInfo } from '../data'
import type { PackType } from './svg'
import { Download } from './icons'
import { getSvg, LoadIconSvgs } from './svg'
export async function LoadIconSvgs(icons: string[]) {
return await Promise.all(
icons
.filter(Boolean)
.sort()
.map(async (name) => {
return {
name,
svg: await getSvg(name),
}
}),
)
export async function getSvgSymbol(
collections: CollectionInfo[],
icon: string,
size = '1em',
color = 'currentColor',
) {
const svgMarkup = await getSvg(collections, icon, size, color)
const symbolElem = document.createElementNS('http://www.w3.org/2000/svg', 'symbol')
const node = document.createElement('div') // Create any old element
node.innerHTML = svgMarkup
// Grab the inner HTML and move into a symbol element
symbolElem.innerHTML = node.querySelector('svg')!.innerHTML
symbolElem.setAttribute('viewBox', node.querySelector('svg')!.getAttribute('viewBox')!)
symbolElem.id = icon.replace(/:/, '-') // Simple slugify for quick symbol lookup
return symbolElem?.outerHTML
}
export async function* PrepareIconSvgs(icons: string[], format: 'svg' | 'json', name?: string) {
if (format === 'json') {
const svgs = await LoadIconSvgs(icons)
yield {
name: `${name}.json`,
input: new Blob([JSON.stringify(svgs, null, 2)], { type: 'application/json; charset=utf-8' }),
}
return
}
for (const icon of icons) {
if (!icon)
continue
const svg = await getSvg(icon)
yield {
name: `${normalizeZipFleName(icon)}.svg`,
input: new Blob([svg], { type: 'image/svg+xml' }),
}
}
}
export async function Download(blob: Blob, name: string) {
if (isVSCode) {
blob.arrayBuffer().then(
buffer => vscode.postMessage({
command: 'download',
name,
text: bufferToString(buffer),
}),
)
}
else {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = name
a.click()
a.remove()
}
}
export async function PackSVGSprite(icons: string[], options: any = {}) {
export async function PackSVGSprite(
collections: CollectionInfo[],
icons: string[],
options: any = {},
) {
if (!icons.length)
return
const data = await LoadIconSvgs(icons)
const data = await LoadIconSvgs(collections, icons)
let symbols = ''
for (const { name } of data)
symbols += `${await getSvgSymbol(name, options.size, options.color)}\n`
symbols += `${await getSvgSymbol(collections, name, options.size, options.color)}\n`
const svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
@ -85,96 +46,112 @@ ${symbols}
Download(blob, 'sprite.svg')
}
function normalizeZipFleName(svgName: string): string {
return svgName.replace(':', '-')
}
export async function PackIconFont(icons: string[], options: any = {}) {
export async function PackIconFont(
collections: CollectionInfo[],
icons: string[],
options: any = {},
) {
if (!icons.length)
return
const [data, { SvgPacker }] = await Promise.all([
LoadIconSvgs(icons),
import('svg-packer'),
])
const result = await SvgPacker({
fontName: 'Iconify Explorer Font',
fileName: 'iconfont',
cssPrefix: 'i',
...options,
icons: data,
const { packerWorker } = await import('./pack-worker-client')
return new Promise<void>((resolve, reject) => {
packerWorker.addEventListener('message', (event) => {
if (event.data.error) {
reject(event.data.error)
return
}
const { blob, name } = event.data as { blob: ArrayBuffer, name: string }
Download(
new Blob([blob]),
name,
)
resolve()
}, { once: true })
const arrayBuffer = createArrayBufferFromCollections(collections)
packerWorker.postMessage({
operation: 'pack-font-zip',
collections: arrayBuffer,
payload: {
icons: toRaw(icons),
...toRaw(options),
},
}, [arrayBuffer])
})
Download(result.zip.blob, result.zip.name)
}
export async function PackSvgZip(icons: string[], name: string) {
if (!icons.length)
return
Download(
await import('client-zip').then(({ downloadZip }) => downloadZip(
PrepareIconSvgs(icons, 'svg'),
).blob()),
`${name}.zip`,
)
}
export async function PackJsonZip(icons: string[], name: string) {
if (!icons.length)
return
Download(
await import('client-zip').then(({ downloadZip }) => downloadZip(
PrepareIconSvgs(icons, 'json', name),
).blob()),
`${name}.zip`,
)
}
export type PackType = 'svg' | 'tsx' | 'jsx' | 'vue' | 'json'
async function* PreparePackZip(
export async function PackSvgZip(
collections: CollectionInfo[],
icons: string[],
name: string,
type: PackType,
) {
if (type === 'json' || type === 'svg') {
yield* PrepareIconSvgs(icons, type, name)
if (!icons.length)
return
const { packerWorker } = await import('./pack-worker-client')
return new Promise<void>((resolve, reject) => {
packerWorker.addEventListener('message', (event) => {
if (event.data.error) {
reject(event.data.error)
return
}
for (const name of icons) {
if (!name)
continue
const svg = await getSvg(name)
const componentName = toComponentName(normalizeZipFleName(name))
let content: string
switch (type) {
case 'vue':
content = await SvgToVue(svg, componentName)
break
case 'jsx':
content = await SvgToJSX(svg, componentName, false)
break
case 'tsx':
content = await SvgToTSX(svg, componentName, false)
break
default:
continue
const { blob } = event.data as { blob: ArrayBuffer }
Download(
new Blob([blob]),
`${name}.zip`,
)
resolve()
}, { once: true })
const arrayBuffer = createArrayBufferFromCollections(collections)
packerWorker.postMessage({
operation: 'pack-svg-zip',
collections: arrayBuffer,
payload: {
icons: toRaw(icons),
},
}, [arrayBuffer])
})
}
yield {
name: `${componentName}.${type}`,
input: new Blob([content], { type: 'text/plain' }),
}
export async function PackJsonZip(
collections: CollectionInfo[],
icons: string[],
name: string,
) {
if (!icons.length)
return
const { packerWorker } = await import('./pack-worker-client')
return new Promise<void>((resolve, reject) => {
packerWorker.addEventListener('message', (event) => {
if (event.data.error) {
reject(event.data.error)
return
}
const { blob } = event.data as { blob: ArrayBuffer }
Download(
new Blob([blob]),
`${name}.zip`,
)
resolve()
}, { once: true })
const arrayBuffer = createArrayBufferFromCollections(collections)
packerWorker.postMessage({
operation: 'pack-json-zip',
collections: arrayBuffer,
payload: {
icons: toRaw(icons),
name,
},
}, [arrayBuffer])
})
}
export async function PackZip(
collections: CollectionInfo[],
icons: string[],
name: string,
type: PackType = 'svg',
@ -182,10 +159,36 @@ export async function PackZip(
if (!icons.length)
return
const { packerWorker } = await import('./pack-worker-client')
return new Promise<void>((resolve, reject) => {
packerWorker.addEventListener('message', (event) => {
if (event.data.error) {
reject(event.data.error)
return
}
const { blob } = event.data as { blob: ArrayBuffer }
Download(
await import('client-zip').then(({ downloadZip }) => downloadZip(
PreparePackZip(icons, name, type),
).blob()),
new Blob([blob]),
`${name}-${type}.zip`,
)
resolve()
}, { once: true })
const arrayBuffer = createArrayBufferFromCollections(collections)
packerWorker.postMessage({
operation: 'pack-zip',
collections: arrayBuffer,
payload: {
icons: toRaw(icons),
name,
type,
},
}, [arrayBuffer])
})
}
function createArrayBufferFromCollections(
collections: CollectionInfo[],
) {
return new TextEncoder().encode(JSON.stringify(collections)).buffer
}

222
src/utils/svg/helpers.ts Normal file
View File

@ -0,0 +1,222 @@
import type { Node } from 'ultrahtml'
import type { CollectionInfo } from '../../data'
import { encodeSvgForCss } from '@iconify/utils'
import { parse, transformSync } from 'ultrahtml'
import Base64 from './base64'
import { HtmlToJSX } from './htmlToJsx'
import { getSvg } from './loader'
import { prettierCode } from './prettier'
export type PackType = 'svg' | 'tsx' | 'jsx' | 'vue' | 'json'
export function normalizeZipFleName(svgName: string): string {
return svgName.replace(':', '-')
}
export function toComponentName(icon: string) {
return icon.split(/[:\-_]/).filter(Boolean).map(s => s[0].toUpperCase() + s.slice(1).toLowerCase()).join('')
}
export function ClearSvg(svgCode: string, reactJSX?: boolean) {
const result = transformSync(parse(svgCode).children[0] as Node, [
(node: Node): Node => {
if (node.name !== 'svg')
return node
const attributes = node.attributes || {}
// keep only 'viewBox', 'width', 'height', 'focusable', 'xmlns', 'xlink' attributes
const allowedAttributes = ['viewBox', 'width', 'height', 'focusable', 'xmlns', 'xlink']
for (const key of Object.keys(attributes)) {
if (!allowedAttributes.includes(key)) {
delete attributes[key]
}
}
node.attributes = attributes
return node
},
])
return HtmlToJSX(result, reactJSX)
}
export function SvgToDataURL(svg: string) {
const base64 = `data:image/svg+xml;base64,${Base64.encode(svg)}`
const plain = `data:image/svg+xml,${encodeSvgForCss(svg)}`
// Return the shorter of the two data URLs
return base64.length < plain.length ? base64 : plain
}
export function SvgToJSX(svg: string, name: string, snippet: boolean) {
const code = `
export function ${name}(props) {
return (
${ClearSvg(svg, true).replace(/<svg (.*?)>/, '<svg $1 {...props}>')}
)
}`
if (snippet)
return prettierCode(code, 'babel-ts')
else
return prettierCode(`import React from 'react'\n${code}\nexport default ${name}`, 'babel-ts')
}
export function SvgToTSX(svg: string, name: string, snippet: boolean, reactJSX = true) {
let code = `
export function ${name}(props: SVGProps<SVGSVGElement>) {
return (
${ClearSvg(svg, reactJSX).replace(/<svg (.*?)>/, '<svg $1 {...props}>')}
)
}`
code = snippet ? code : `import React, { SVGProps } from 'react'\n${code}\nexport default ${name}`
return prettierCode(code, 'babel-ts')
}
export function SvgToQwik(svg: string, name: string, snippet: boolean) {
let code = `
export function ${name}(props: QwikIntrinsicElements['svg'], key: string) {
return (
${ClearSvg(svg, false).replace(/<svg (.*?)>/, '<svg $1 {...props} key={key}>')}
)
}`
code = snippet ? code : `import type { QwikIntrinsicElements } from '@builder.io/qwik'\n${code}\nexport default ${name}`
return prettierCode(code, 'babel-ts')
}
export function SvgToVue(svg: string, name: string, isTs?: boolean) {
const content = `
<template>
${ClearSvg(svg)}
</template>
<script>
export default {
name: '${name}'
}
</script>`
const code = isTs ? content.replace('<script>', '<script lang="ts">') : content
return prettierCode(code, 'vue')
}
export function SvgToSolid(svg: string, name: string, snippet: boolean) {
let code = `
export function ${name}(props: JSX.IntrinsicElements['svg']) {
return (
${svg.replace(/<svg (.*?)>/, '<svg $1 {...props}>')}
)
}`
code = snippet ? code : `import type { JSX } from 'solid-js'\n${code}\nexport default ${name}`
return prettierCode(code, 'babel-ts')
}
export function SvgToSvelte(svg: string) {
return `${svg.replace(/<svg (.*?)>/, '<svg $1 {...$$$props}>')}`
}
export function SvgToAstro(svg: string) {
return `
---
const props = Astro.props
---
${svg.replace(/<svg (.*?)>/, '<svg $1 {...props}>')}
`
}
export function SvgToReactNative(svg: string, name: string, snippet: boolean) {
function replaceTags(svg: string, replacements: {
from: string
to: string
}[]): string {
let result = svg
replacements.forEach(({ from, to }) => {
result = result.replace(new RegExp(`<${from}(.*?)>`, 'g'), `<${to}$1>`)
.replace(new RegExp(`</${from}>`, 'g'), `</${to}>`)
})
return result
}
function generateImports(usedComponents: string[]): string {
// Separate Svg from the other components
const svgIndex = usedComponents.indexOf('Svg')
if (svgIndex !== -1)
usedComponents.splice(svgIndex, 1)
// Join all other component names with a comma and wrap them in curly braces
const componentsString = usedComponents.length > 0 ? `{ ${usedComponents.join(', ')} }` : ''
// Return the consolidated import statement, ensuring Svg is imported as a default import
return `import Svg, ${componentsString} from 'react-native-svg';`
}
const replacements: {
from: string
to: string
}[] = [
{ from: 'svg', to: 'Svg' },
{ from: 'path', to: 'Path' },
{ from: 'g', to: 'G' },
{ from: 'circle', to: 'Circle' },
{ from: 'rect', to: 'Rect' },
{ from: 'line', to: 'Line' },
{ from: 'polyline', to: 'Polyline' },
{ from: 'polygon', to: 'Polygon' },
{ from: 'ellipse', to: 'Ellipse' },
{ from: 'text', to: 'Text' },
{ from: 'tspan', to: 'Tspan' },
{ from: 'textPath', to: 'TextPath' },
{ from: 'defs', to: 'Defs' },
{ from: 'use', to: 'Use' },
{ from: 'symbol', to: 'Symbol' },
{ from: 'linearGradient', to: 'LinearGradient' },
{ from: 'radialGradient', to: 'RadialGradient' },
{ from: 'stop', to: 'Stop' },
]
const reactNativeSvgCode = replaceTags(ClearSvg(svg, true), replacements)
.replace(/className=/g, '')
.replace(/href=/g, 'xlinkHref=')
.replace(/clip-path=/g, 'clipPath=')
.replace(/fill-opacity=/g, 'fillOpacity=')
.replace(/stroke-width=/g, 'strokeWidth=')
.replace(/stroke-linecap=/g, 'strokeLinecap=')
.replace(/stroke-linejoin=/g, 'strokeLinejoin=')
.replace(/stroke-miterlimit=/g, 'strokeMiterlimit=')
const svgComponents = replacements.map(({ to }) => to)
const imports = generateImports(svgComponents.filter(component => reactNativeSvgCode.includes(component)))
let code = `
${imports}
export function ${name}(props) {
return (
${reactNativeSvgCode}
)
}`
if (!snippet)
code = `import React from 'react';\n${code}\nexport default ${name}`
return prettierCode(code, 'babel-ts')
}
export async function LoadIconSvgs(
collections: CollectionInfo[],
icons: string[],
) {
return await Promise.all(
icons
.filter(Boolean)
.sort()
.map(async (name) => {
return {
name,
svg: await getSvg(collections, name),
}
}),
)
}

21
src/utils/svg/index.ts Normal file
View File

@ -0,0 +1,21 @@
export { default } from './base64'
export { bufferToString } from './bufferToString'
export type { PackType } from './helpers'
export {
ClearSvg,
LoadIconSvgs,
normalizeZipFleName,
SvgToAstro,
SvgToDataURL,
SvgToJSX,
SvgToQwik,
SvgToReactNative,
SvgToSolid,
SvgToSvelte,
SvgToTSX,
SvgToVue,
toComponentName,
} from './helpers'
export { HtmlToJSX } from './htmlToJsx'
export { API_ENTRY, getLicenseComment, getSvg, getSvgLocal } from './loader'
export { prettierCode } from './prettier'

46
src/utils/svg/loader.ts Normal file
View File

@ -0,0 +1,46 @@
import type { CollectionInfo } from '../../data'
import { buildIcon, loadIcon } from 'iconify-icon'
export const API_ENTRY = 'https://api.iconify.design'
export async function getLicenseComment(collections: CollectionInfo[], icon: string) {
const [id] = icon.split(':')
const collection = collections.find(i => i.id === id)
if (!collection) {
return ''
}
return `<!-- Icon from ${collection?.name} by ${collection?.author?.name} - ${collection?.license?.url} -->`
}
export async function getSvgLocal(
collections: CollectionInfo[],
icon: string,
size = '1em',
color = 'currentColor',
) {
const data = await loadIcon(icon)
if (!data)
return
const built = buildIcon(data, { height: size })
if (!built)
return
const license = await getLicenseComment(collections, icon)
const xlink = built.body.includes('xlink:') ? ' xmlns:xlink="http://www.w3.org/1999/xlink"' : ''
return `<svg xmlns="http://www.w3.org/2000/svg"${xlink} ${Object.entries(built.attributes).map(([k, v]) => `${k}="${v}"`).join(' ')}>${license}${built.body}</svg>`.replaceAll('currentColor', color)
}
export async function getSvg(
collections: CollectionInfo[],
icon: string,
size = '1em',
color = 'currentColor',
) {
const local = await getSvgLocal(collections, icon, size, color)
if (local)
return local
const mode = import.meta.env.DEV && !PWA ? 'no-cors' : undefined
return await fetch(`${API_ENTRY}/${icon}.svg?inline=false&height=${size}&color=${encodeURIComponent(color)}`, {
mode,
}).then(r => r.text()) || ''
}

View File

@ -1,5 +1,5 @@
import type { BuiltInParserName } from 'prettier'
import { isElectron } from '../env'
import { isElectron } from '../../env'
export async function prettierCode(code: string, parser: BuiltInParserName) {
if (!isElectron)

203
src/utils/worker/index.ts Normal file
View File

@ -0,0 +1,203 @@
/// <reference lib="webworker" />
import type { CollectionInfo } from '../../data'
import type { PackType } from '../svg'
import type { PackOperation, WorkerPackMessage } from './types'
import { downloadZip } from 'client-zip'
import {
getSvg,
LoadIconSvgs,
normalizeZipFleName,
SvgToJSX,
SvgToTSX,
SvgToVue,
toComponentName,
} from '../svg'
globalThis.onmessage = async (event: MessageEvent<WorkerPackMessage<PackOperation>>) => {
const message = event.data
let blob: Blob | undefined
let name: string | undefined
try {
const collections: CollectionInfo[] = JSON.parse(
new TextDecoder().decode(message.collections),
)
if (isPackZipMessage(message)) {
blob = await downloadZip(
PreparePackZip(
collections,
message.payload.icons,
message.payload.name,
message.payload.type,
),
).blob()
}
else if (isPackJsonZipMessage(message)) {
blob = await downloadZip(
PrepareIconSvgs(
collections,
message.payload.icons,
'json',
message.payload.name,
),
).blob()
}
else if (isPackSvgZipMessage(message)) {
blob = await downloadZip(
PrepareIconSvgs(
collections,
message.payload.icons,
'svg',
),
).blob()
}
else if (isPackFontZipMessage(message)) {
const result = await PackIconFont(
collections,
message.payload.icons,
message.payload.options,
)
if (result) {
([blob, name] = result)
}
}
}
catch (e: any) {
console.error('PackWorker: error while generating the zip', e)
globalThis.postMessage({ error: e && 'message' in e ? e.message : String(e) })
return
}
if (blob) {
try {
const arrayBuffer = await blob.arrayBuffer()
globalThis.postMessage({
blob: arrayBuffer,
name,
}, [arrayBuffer])
}
catch (e: any) {
console.error('PackWorker: error while transferring generated zip', e)
globalThis.postMessage({ error: e && 'message' in e ? e.message : String(e) })
}
}
else {
globalThis.postMessage({ error: 'No blob generated' })
}
}
export function isPackZipMessage(
message: WorkerPackMessage<PackOperation>,
): message is WorkerPackMessage<'pack-zip'> {
return message.operation === 'pack-zip'
}
export function isPackJsonZipMessage(
message: WorkerPackMessage<PackOperation>,
): message is WorkerPackMessage<'pack-json-zip'> {
return message.operation === 'pack-json-zip'
}
export function isPackSvgZipMessage(
message: WorkerPackMessage<PackOperation>,
): message is WorkerPackMessage<'pack-svg-zip'> {
return message.operation === 'pack-svg-zip'
}
export function isPackFontZipMessage(
message: WorkerPackMessage<PackOperation>,
): message is WorkerPackMessage<'pack-font-zip'> {
return message.operation === 'pack-font-zip'
}
async function* PrepareIconSvgs(
collections: CollectionInfo[],
icons: string[],
format: 'svg' | 'json',
name?: string,
) {
if (format === 'json') {
const svgs = await LoadIconSvgs(collections, icons)
yield {
name: `${name}.json`,
input: new Blob([JSON.stringify(svgs, null, 2)], { type: 'application/json; charset=utf-8' }),
}
return
}
for (const icon of icons) {
if (!icon)
continue
const svg = await getSvg(collections, icon)
yield {
name: `${normalizeZipFleName(icon)}.svg`,
input: new Blob([svg], { type: 'image/svg+xml' }),
}
}
}
async function* PreparePackZip(
collections: CollectionInfo[],
icons: string[],
name: string,
type: PackType,
) {
if (type === 'json' || type === 'svg') {
yield* PrepareIconSvgs(collections, icons, type, name)
return
}
for (const name of icons) {
if (!name)
continue
const svg = await getSvg(collections, name)
const componentName = toComponentName(normalizeZipFleName(name))
let content: string
switch (type) {
case 'vue':
content = await SvgToVue(svg, componentName)
break
case 'jsx':
content = await SvgToJSX(svg, componentName, false)
break
case 'tsx':
content = await SvgToTSX(svg, componentName, false)
break
default:
continue
}
yield {
name: `${componentName}.${type}`,
input: new Blob([content], { type: 'text/plain' }),
}
}
}
async function PackIconFont(
collections: CollectionInfo[],
icons: string[],
options: any = {},
) {
if (!icons.length)
return
const [data, { SvgPacker }] = await Promise.all([
LoadIconSvgs(collections, icons),
import('svg-packer'),
])
const result = await SvgPacker({
fontName: 'Iconify Explorer Font',
fileName: 'iconfont',
cssPrefix: 'i',
...options,
icons: data,
})
return [result.zip.blob, result.zip.name] as const
}

36
src/utils/worker/types.ts Normal file
View File

@ -0,0 +1,36 @@
import type { PackType } from '../svg'
export type PackOperation = 'pack-zip' | 'pack-json-zip' | 'pack-svg-zip' | 'pack-font-zip'
export interface PackZipPayload {
icons: string[]
name: string
type: PackType
}
export interface PackJsonZipPayload {
icons: string[]
name: string
}
export interface PackSvgZipPayload {
icons: string[]
}
export interface PackFontZipPayload {
icons: string[]
options: any
}
export interface WorkerPackMessage<O extends PackOperation> {
payload: O extends 'pack-zip' ? PackZipPayload :
O extends 'pack-json-zip' ? PackJsonZipPayload :
O extends 'pack-svg-zip' ? PackSvgZipPayload :
O extends 'pack-font-zip' ? PackFontZipPayload :
never
operation: O
collections: ArrayBuffer
}
export interface WorkerPackResponse {
blob: ArrayBuffer
name?: string
}

View File

@ -112,5 +112,14 @@ export default defineConfig(({ mode }) => {
'iconify-icon': resolve(__dirname, 'node_modules/iconify-icon/dist/iconify-icon.mjs'),
},
},
worker: {
format: 'es',
rollupOptions: {
treeshake: true,
},
plugins: () => [
SvgPackerVitePlugin(),
],
},
}
})