shadcn-vue/apps/www/src/registry/crawl-content.ts

354 lines
9.8 KiB
TypeScript

import type { Registry, RegistryFiles } from './schema'
import { readdir, readFile } from 'node:fs/promises'
import { parseSync } from '@oxc-parser/wasm'
import { join, resolve } from 'pathe'
import { compileScript, parse, walk } from 'vue/compiler-sfc'
import { type RegistryStyle, styles } from './registry-styles'
// [Dependency, [...PeerDependencies]]
const DEPENDENCIES = new Map<string, string[]>([
['reka-ui', []],
['@vueuse/core', []],
['vue-sonner', []],
['vaul-vue', []],
['@tanstack/vue-table', []],
['@unovis/vue', ['@unovis/ts']],
['embla-carousel-vue', []],
['vee-validate', ['@vee-validate/zod', 'zod']],
])
// Some dependencies latest tag were not compatible with Vue3.
// const DEPENDENCIES_WITH_TAGS = new Map<string, string>([
// ['v-calendar', 'v-calendar@next'],
// ])
const REGISTRY_DEPENDENCY = '@/'
type ArrayItem<T> = T extends Array<infer X> ? X : never
type RegistryItem = ArrayItem<Registry>
export async function buildRegistry() {
const registryRootPath = resolve('src', 'registry')
const registry: Registry = []
for (const { name: style } of styles) {
const uiPath = resolve(registryRootPath, style, 'ui')
const examplePath = resolve(registryRootPath, style, 'example')
const blockPath = resolve(registryRootPath, style, 'block')
// const hookPath = resolve(registryRootPath, style, 'hook')
const [ui, example, block] = await Promise.all([
crawlUI(uiPath),
crawlExample(examplePath),
crawlBlock(blockPath),
// crawlHook(hookPath),
])
registry.push(...ui, ...example, ...block)
}
return registry
}
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 crawlExample(rootPath: string) {
const type = `registry:example` as const
const dir = await readdir(rootPath, { withFileTypes: true })
const registry: Registry = []
for (const dirent of dir) {
if (!dirent.name.endsWith('.vue') || !dirent.isFile())
continue
const [name] = dirent.name.split('.vue')
const filepath = join(rootPath, dirent.name)
const source = await readFile(filepath, { encoding: 'utf8' })
const relativePath = join('example', dirent.name)
const file = {
name: dirent.name,
content: source,
path: relativePath,
// style,
target: '',
type,
}
const { dependencies, registryDependencies } = await getFileDependencies(filepath, source)
registry.push({
name,
type,
// style,
files: [file],
registryDependencies: Array.from(registryDependencies),
dependencies: Array.from(dependencies),
})
}
return registry
}
async function crawlBlock(rootPath: string) {
const type = `registry:block` as const
const dir = await readdir(rootPath, { withFileTypes: true })
const registry: Registry = []
for (const dirent of dir) {
if (!dirent.isFile()) {
const result = await buildBlockRegistry(
`${rootPath}/${dirent.name}`,
dirent.name,
)
// console.log(result.name, result.files.length)
if (result.files.length) {
registry.push(result)
}
continue
}
if (!dirent.name.endsWith('.vue') || !dirent.isFile())
continue
const [name] = dirent.name.split('.vue')
const filepath = join(rootPath, dirent.name)
const source = await readFile(filepath, { encoding: 'utf8' })
const relativePath = join('block', dirent.name)
const target = 'pages/dashboard/index.vue'
const file = {
name: dirent.name,
content: source,
path: relativePath,
target,
type,
}
const { dependencies, registryDependencies } = await getFileDependencies(filepath, source)
registry.push({
name,
type,
files: [file],
registryDependencies: Array.from(registryDependencies),
dependencies: Array.from(dependencies),
})
}
return registry
}
async function crawlHook(rootPath: string, style: RegistryStyle) {
const type = `registry:hook` as const
const dir = await readdir(rootPath, { withFileTypes: true })
const registry: Registry = []
for (const dirent of dir) {
if (!dirent.isFile())
continue
const [name] = dirent.name.split('.vue.ts')
const filepath = join(rootPath, dirent.name)
const source = await readFile(filepath, { encoding: 'utf8' })
const relativePath = join('hook', dirent.name)
const file = {
name: dirent.name,
content: source,
path: relativePath,
style,
target: dirent.name,
type,
}
const { dependencies, registryDependencies } = await getFileDependencies(filepath, source)
registry.push({
name,
type,
files: [file],
registryDependencies: Array.from(registryDependencies),
dependencies: Array.from(dependencies),
})
}
return registry
}
async function buildUIRegistry(componentPath: string, componentName: string) {
const dir = await readdir(componentPath, {
withFileTypes: true,
})
const files: RegistryFiles[] = []
const dependencies = new Set<string>()
const registryDependencies = new Set<string>()
const type = 'registry:ui'
for (const dirent of dir) {
if (!dirent.isFile())
continue
const filepath = join(componentPath, dirent.name)
const relativePath = join('ui', componentName, dirent.name)
const source = await readFile(filepath, { encoding: 'utf8' })
const target = ''
files.push({ content: source, path: relativePath, type, target })
// only grab deps from the vue files
if (dirent.name === 'index.ts')
continue
const deps = await getFileDependencies(filepath, source)
if (!deps)
continue
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 buildBlockRegistry(blockPath: string, blockName: string) {
const dir = await readdir(blockPath, { withFileTypes: true, recursive: true })
const files: RegistryFiles[] = []
const dependencies = new Set<string>()
const registryDependencies = new Set<string>()
for (const dirent of dir) {
if (!dirent.isFile())
continue
const isPage = dirent.name === 'page.vue'
const type = isPage ? 'registry:page' : 'registry:component'
const compPath = isPage ? dirent.name : `components/${dirent.name}`
const filepath = join(blockPath, compPath)
const relativePath = join('block', blockName, compPath)
const source = await readFile(filepath, { encoding: 'utf8' })
const target = isPage ? `pages/dashboard/index.vue` : ''
files.push({ content: source, path: relativePath, type, target })
const deps = await getFileDependencies(filepath, source)
if (!deps)
continue
deps.dependencies.forEach(dep => dependencies.add(dep))
deps.registryDependencies.forEach(dep => registryDependencies.add(dep))
}
return {
type: 'registry:block',
files,
name: blockName,
registryDependencies: Array.from(registryDependencies),
dependencies: Array.from(dependencies),
} satisfies RegistryItem
}
async function getFileDependencies(filename: string, sourceCode: string) {
const registryDependencies = new Set<string>()
const dependencies = new Set<string>()
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) && !source.endsWith('.vue')) {
const component = source.split('/').at(-1)!
registryDependencies.add(component)
}
}
if (filename.endsWith('.ts')) {
const ast = parseSync(sourceCode, {
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(sourceCode, { 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 }
}
export async function getBlockMetadata(filename: string, sourceCode: string) {
const metadata = {
description: null as string | null,
iframeHeight: null as string | null,
containerClass: null as string | null,
}
if (filename.endsWith('.vue')) {
const { descriptor } = parse(sourceCode, { filename })
if (descriptor.script?.content) {
const ast = compileScript(descriptor, { id: '' })
walk(ast.scriptAst, {
enter(node: any) {
const declaration = node.declaration
// Check if the declaration is a variable declaration
if (declaration?.type === 'VariableDeclaration') {
// Extract variable names and their values
declaration.declarations.forEach((decl: any) => {
// @ts-expect-error ignore missing type
metadata[decl.id.name] = decl.init ? decl.init.value : null
})
}
},
})
}
}
return metadata
}