import { readFile, readdir } from 'node:fs/promises' import { join, normalize, resolve } from 'pathe' import { compileScript, parse } from 'vue/compiler-sfc' import { parseSync } from '@oxc-parser/wasm' import type { Registry } from '../../lib/registry' const DEPENDENCIES = new Map([ ['@vueuse/core', []], ['vue-sonner', []], ['vaul-vue', []], ['v-calendar', []], ['@tanstack/vue-table', []], ['embla-carousel-vue', ['embla-carousel']], ['vee-validate', ['@vee-validate/zod', 'zod']], ]) // Some dependencies latest tag were not compatible with Vue3. const DEPENDENCIES_WITH_TAGS = new Map([ ['v-calendar', 'v-calendar@next'], ]) const REGISTRY_DEPENDENCY = '@/' type ArrayItem = T extends Array ? X : never type RegistryItem = ArrayItem export async function buildRegistry() { const ui_path = resolve('./src/lib/registry/default/ui') const uiRegistry = await crawlUI(ui_path) const example_path = resolve('./src/lib/registry/default/example') const exampleRegistry = await crawlDirectory(example_path, 'example') const block_path = resolve('./src/lib/registry/default/block') const blockRegistry = await crawlDirectory(block_path, 'block') return uiRegistry.concat(exampleRegistry).concat(blockRegistry) } async function crawlUI(rootPath: string) { const dir = await readdir(rootPath, { recursive: true, withFileTypes: true, }) const uiRegistry: Registry = [] for (const dirent of dir) { if (!dirent.isDirectory()) continue const componentPath = resolve(rootPath, dirent.name) const ui = await buildUIRegistry(componentPath, dirent.name) uiRegistry.push(ui) } return uiRegistry } async function crawlDirectory(rootPath: string, typeName: 'example' | 'block') { const type = `components:${typeName}` as const const dir = await readdir(rootPath, { recursive: true, withFileTypes: true, }) const registry: Registry = [] for (const dirent of dir) { if (dirent.name === 'index.ts') continue if (dirent.isFile()) { const [name] = dirent.name.split('.vue') const file_path = join(typeName, normalize(dirent.path).split(`/${typeName}`)[1], dirent.name) const { dependencies, registryDependencies } = await getDependencies(join(dirent.path, dirent.name)) registry.push({ name, type, files: [file_path], registryDependencies: Array.from(registryDependencies), dependencies: Array.from(dependencies), }) } // ignoring examples with directories for now... // if (dirent.isDirectory()) { // const componentPath = resolve(rootPath, dirent.name); // const ui = await buildUIRegistry(componentPath, dirent.name); // registry.push({ // ...ui, // type // }); // } } return registry } async function buildUIRegistry(componentPath: string, componentName: string) { const dir = await readdir(componentPath, { withFileTypes: true, }) const files: string[] = [] const dependencies = new Set() const registryDependencies = new Set() const type = 'components:ui' for (const dirent of dir) { if (!dirent.isFile()) continue const file_path = join('ui', componentName, dirent.name) files.push(file_path) files.sort() // only grab deps from the vue files if (dirent.name === 'index.ts') continue const deps = await getDependencies(join(componentPath, dirent.name)) deps.dependencies.forEach(dep => dependencies.add(dep)) deps.registryDependencies.forEach(dep => registryDependencies.add(dep), ) } return { name: componentName, type, files, registryDependencies: Array.from(registryDependencies), dependencies: Array.from(dependencies), } satisfies RegistryItem } async function getDependencies(filename: string) { const code = await readFile(filename, { encoding: 'utf8' }) const registryDependencies = new Set() const dependencies = new Set() const populateDeps = (source: string) => { const peerDeps = DEPENDENCIES.get(source) const taggedDeps = DEPENDENCIES_WITH_TAGS.get(source) if (peerDeps !== undefined) { if (taggedDeps !== undefined) dependencies.add(taggedDeps) else dependencies.add(source) peerDeps.forEach(dep => dependencies.add(dep)) } if (source.startsWith(REGISTRY_DEPENDENCY)) { const component = source.split('/').at(-1)! registryDependencies.add(component) } } if (filename.endsWith('.ts')) { const ast = parseSync(code, { sourceType: 'module', sourceFilename: filename, }) const sources = ast.program.body.filter((i: any) => i.type === 'ImportDeclaration').map((i: any) => i.source) sources.forEach((source: any) => { populateDeps(source.value) }) } else { const parsed = parse(code, { filename }) if (parsed.descriptor.script?.content || parsed.descriptor.scriptSetup?.content) { const compiled = compileScript(parsed.descriptor, { id: '' }) Object.values(compiled.imports!).forEach((value) => { populateDeps(value.source) }) } } return { registryDependencies, dependencies } }