feat: use ESM svg-packer with Vite (#364)

Co-authored-by: Anthony Fu <github@antfu.me>
This commit is contained in:
Joaquín Sánchez 2025-05-28 07:20:12 +02:00 committed by GitHub
parent 5dbe68f16b
commit 5e8ea588db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 1209 additions and 725 deletions

View File

@ -7,8 +7,6 @@
<link rel="icon" href="/favicon.svg" />
<link rel="icon" href="/favicon-dark.svg" media="(prefers-color-scheme: light)" />
<link rel="search" type="application/opensearchdescription+xml" href="/search.xml" title="Icônes" />
<script src="/lib/svg-packer.js" defer></script>
<script src="/lib/jszip.min.js" defer></script>
</head>
<body class="dragging bg-base color-base">
<div id="app"></div>

View File

@ -16,8 +16,6 @@
},
"scripts": {
"postinstall": "esno scripts/prepare.ts",
"prebuild": "esno scripts/prebuild.ts",
"postbuild": "esno scripts/postbuild.ts",
"lint": "eslint .",
"dev": "vite --port 3333 --open",
"dev-pwa": "SW_DEV=true vite --port 3333",
@ -46,34 +44,24 @@
"@types/file-saver": "^2.0.7",
"@types/fs-extra": "^11.0.4",
"@vitejs/plugin-vue": "^5.2.3",
"client-zip": "^2.5.0",
"dayjs": "^1.11.13",
"eslint": "^9.23.0",
"eslint-plugin-format": "^1.0.1",
"esno": "^4.8.0",
"fast-glob": "^3.3.3",
"fs-extra": "^11.3.0",
"jszip": "^3.10.1",
"lru-cache": "^11.0.2",
"pnpm": "^10.6.5",
"shiki": "^3.2.1",
"svg-packer": "^0.0.3",
"svg-packer": "^1.0.0",
"typescript": "^5.8.2",
"unocss": "^66.1.0-beta.6",
"unplugin-auto-import": "^19.1.1",
"unplugin-vue-components": "^28.4.1",
"vite": "^6.2.2",
"vite": "^6.3.5",
"vite-plugin-pages": "^0.32.5",
"vite-plugin-pwa": "^0.21.2",
"vite-plugin-pwa": "^1.0.0",
"vue-tsc": "^2.2.8"
},
"pnpm": {
"neverBuiltDependencies": [
"electron",
"electron-builder",
"ttf2woff2",
"vite-plugin-electron",
"vite-plugin-electron-renderer",
"vite-plugin-esmodule"
]
}
}

1717
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +1,4 @@
packages:
- electron
onlyBuiltDependencies:
- ttf2woff2

View File

@ -1,15 +0,0 @@
import { promises as fs } from 'node:fs'
;
(async () => {
const path = './dist/index.html'
const file = await fs.readFile(path, 'utf-8')
await fs.writeFile(path, file.replace(/\/\/lib/g, '/lib'), 'utf-8')
})()
;(async () => {
const path = './index.html'
const file = await fs.readFile(path, 'utf-8')
await fs.writeFile(path, file.replace(/\/\/lib/g, '/lib'), 'utf-8')
})()

View File

@ -1,9 +0,0 @@
import { promises as fs } from 'node:fs'
;
(async () => {
const path = './index.html'
const file = await fs.readFile(path, 'utf-8')
await fs.writeFile(path, file.replace(/\/lib/g, '//lib'), 'utf-8')
})()

View File

@ -77,23 +77,4 @@ async function prepareJSON() {
await fs.writeJSON(path.join(infoOut, 'collections-info.json'), collections)
}
async function copyLibs() {
const modules = path.resolve(__dirname, '../node_modules')
await fs.copy(
path.join(modules, 'svg-packer/dist/index.browser.js'),
path.join(out, 'lib/svg-packer.js'),
)
await fs.copy(
path.join(modules, 'jszip/dist/jszip.min.js'),
path.join(out, 'lib/jszip.min.js'),
)
}
async function prepare() {
await copyLibs()
await prepareJSON()
}
prepare()
prepareJSON()

3
src/shims.d.ts vendored
View File

@ -1,7 +1,4 @@
interface Window {
JSZip: import('jszip')
SvgPacker: (options: any) => Promise<any>
// for vscode
baseURI?: string
staticURI?: string

View File

@ -23,6 +23,29 @@ export async function LoadIconSvgs(icons: string[]) {
)
}
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(
@ -62,11 +85,19 @@ ${symbols}
Download(blob, 'sprite.svg')
}
function normalizeZipFleName(svgName: string): string {
return svgName.replace(':', '-')
}
export async function PackIconFont(icons: string[], options: any = {}) {
if (!icons.length)
return
const data = await LoadIconSvgs(icons)
const result = await window.SvgPacker({
const [data, { SvgPacker }] = await Promise.all([
LoadIconSvgs(icons),
import('svg-packer'),
])
const result = await SvgPacker({
fontName: 'Iconify Explorer Font',
fileName: 'iconfont',
cssPrefix: 'i',
@ -80,32 +111,67 @@ export async function PackIconFont(icons: string[], options: any = {}) {
export async function PackSvgZip(icons: string[], name: string) {
if (!icons.length)
return
const data = await LoadIconSvgs(icons)
const zip = new window.JSZip()
for (const { name, svg } of data)
zip.file(`${name}.svg`, svg)
const blob = await zip.generateAsync({ type: 'blob' })
Download(blob, `${name}.zip`)
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
const data = await LoadIconSvgs(icons)
const zip = new window.JSZip()
zip.file(`${name}.json`, JSON.stringify(data, null, 2))
const blob = await zip.generateAsync({ type: 'blob' })
Download(blob, `${name}.zip`)
Download(
await import('client-zip').then(({ downloadZip }) => downloadZip(
PrepareIconSvgs(icons, 'json', name),
).blob()),
`${name}.zip`,
)
}
export type PackType = 'svg' | 'tsx' | 'jsx' | 'vue' | 'json'
function normalizeZipFleName(svgName: string): string {
return svgName.replace(':', '-')
async function* PreparePackZip(
icons: string[],
name: string,
type: PackType,
) {
if (type === 'json' || type === 'svg') {
yield* PrepareIconSvgs(icons, type, name)
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
}
yield {
name: `${componentName}.${type}`,
input: new Blob([content], { type: 'text/plain' }),
}
}
}
export async function PackZip(
@ -115,39 +181,11 @@ export async function PackZip(
) {
if (!icons.length)
return
const data = await LoadIconSvgs(icons)
const zip = new window.JSZip()
const zipActions: Record<PackType, (name: string, svg: string) => void | (() => void)> = {
vue(name: string, svg: string) {
name = toComponentName(name)
zip.file(`${name}.vue`, SvgToVue(svg, name))
},
jsx(name: string, svg: string) {
name = toComponentName(name)
zip.file(`${name}.jsx`, SvgToJSX(svg, name, false))
},
tsx(name: string, svg: string) {
name = toComponentName(name)
zip.file(`${name}.tsx`, SvgToTSX(svg, name, false))
},
svg(name: string, svg: string) {
zip.file(`${name}.svg`, svg)
},
json() {
zip.file(`${name}.json`, JSON.stringify(data, null, 2))
},
}
const action = zipActions[type]
if (type === 'json') {
(action as () => void)()
}
else {
for (const { name, svg } of data)
action(normalizeZipFleName(name), svg)
}
const blob = await zip.generateAsync({ type: 'blob' })
Download(blob, `${name}-${type}.zip`)
Download(
await import('client-zip').then(({ downloadZip }) => downloadZip(
PreparePackZip(icons, name, type),
).blob()),
`${name}-${type}.zip`,
)
}

View File

@ -4,6 +4,7 @@ import process from 'node:process'
import Vue from '@vitejs/plugin-vue'
import dayjs from 'dayjs'
import fg from 'fast-glob'
import { SvgPackerVitePlugin } from 'svg-packer/vite'
import UnoCSS from 'unocss/vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
@ -61,6 +62,7 @@ export default defineConfig(({ mode }) => {
],
dts: 'src/auto-imports.d.ts',
}),
SvgPackerVitePlugin(),
!isElectron && VitePWA({
strategies: 'injectManifest',
srcDir: 'src',
@ -83,7 +85,8 @@ export default defineConfig(({ mode }) => {
],
},
injectManifest: {
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
// collections-meta.json ~7.5MB
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
},
integration: {
configureOptions(viteConfig, options) {