Files
shadcn-ui/apps/v4/scripts/build-icons.ts
shadcn 1994caba0b perf: dev server (#10904)
* perf: dev server

* fix
2026-06-10 11:10:01 +04:00

147 lines
3.6 KiB
TypeScript

#!/usr/bin/env tsx
import * as fs from "fs"
import * as path from "path"
import { iconLibraries, type IconLibraryName } from "shadcn/icons"
type IconUsage = Record<IconLibraryName, Set<string>>
function findTsxFiles(dir: string) {
const files: string[] = []
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
files.push(...findTsxFiles(fullPath))
} else if (entry.isFile() && entry.name.endsWith(".tsx")) {
files.push(fullPath)
}
}
return files
}
function scanIconUsage() {
const iconUsage: IconUsage = Object.keys(iconLibraries).reduce((acc, key) => {
acc[key as IconLibraryName] = new Set()
return acc
}, {} as IconUsage)
const registryBasesDir = path.join(process.cwd(), "registry/bases")
const files = findTsxFiles(registryBasesDir)
const libraryNames = Object.values(iconLibraries)
.map((lib) => lib.name)
.join("|")
const iconPlaceholderRegex = new RegExp(
`<IconPlaceholder\\s+([^>]*?)(?:${libraryNames})=["']([^"']+)["']([^>]*?)\\/?>`,
"g"
)
for (const file of files) {
const content = fs.readFileSync(file, "utf-8")
let match
while ((match = iconPlaceholderRegex.exec(content)) !== null) {
const fullMatch = match[0]
for (const [libraryName, config] of Object.entries(iconLibraries)) {
const attrMatch = fullMatch.match(
new RegExp(`${config.name}=["']([^"']+)["']`)
)
if (attrMatch) {
iconUsage[libraryName as IconLibraryName].add(attrMatch[1])
}
}
}
}
return iconUsage
}
function generateIconFiles(iconUsage: IconUsage) {
const outputDir = path.join(process.cwd(), "registry/icons")
const written: string[] = []
Object.entries(iconLibraries).forEach(([libraryName, config]) => {
const icons = Array.from(iconUsage[libraryName as IconLibraryName]).sort()
if (icons.length === 0) {
return
}
const content = `// Auto-generated by scripts/build-icons.ts
${icons.map((icon) => `export { ${icon} } from "${config.export}"`).join("\n")}
`
const filename = `__${libraryName}__.ts`
const filepath = path.join(outputDir, filename)
// Skip unchanged files to avoid mtime bumps that trigger
// unnecessary Turbopack invalidations in watch mode.
if (
fs.existsSync(filepath) &&
fs.readFileSync(filepath, "utf-8") === content
) {
return
}
fs.writeFileSync(filepath, content)
written.push(` - ${config.title}: ${icons.length} icons`)
})
if (written.length > 0) {
console.log("✓ Generated icon files:")
written.forEach((line) => console.log(line))
}
}
function main() {
const iconUsage = scanIconUsage()
generateIconFiles(iconUsage)
}
const isWatchMode = process.argv.includes("--watch")
if (isWatchMode) {
const REGISTRY_DIR = path.join(process.cwd(), "registry/bases")
async function startWatcher() {
const { default: chokidar } = await import("chokidar")
main()
const watcher = chokidar.watch(REGISTRY_DIR, {
ignored: /(^|[/\\])\../,
persistent: true,
ignoreInitial: true,
})
const rebuild = (filename: string) => {
if (!filename.endsWith(".tsx")) return
try {
main()
} catch (error) {
console.error("❌ Icons build failed:", error)
}
}
watcher.on("error", (error) => {
console.error("❌ Watcher error:", error)
})
watcher.on("change", rebuild)
watcher.on("add", rebuild)
process.on("SIGINT", async () => {
await watcher.close()
process.exit(0)
})
}
startWatcher()
} else {
main()
}