feat: Replace DB component tools with filesystem-based user-components/
Components are now .vue files in user-components/<folder>/ parsed at runtime. Replaces 6 DB MCP tools with 2 (list_fs_components, load_fs_component). Adds vue-parser, fs-components API, and file watcher for live reload.
This commit is contained in:
117
server/services/vue-parser.ts
Normal file
117
server/services/vue-parser.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Vue File Parser
|
||||
* Extracts <template>, <script setup>, <style> from .vue files
|
||||
* and converts them into VueComponentDefinition objects.
|
||||
*/
|
||||
|
||||
import { join } from 'path'
|
||||
import { readdirSync, readFileSync, existsSync, statSync } from 'fs'
|
||||
|
||||
export interface VueComponentDefinition {
|
||||
id: string
|
||||
name: string
|
||||
template: string
|
||||
setup?: string
|
||||
style?: string
|
||||
props?: string[]
|
||||
imports?: string[]
|
||||
tags?: string[]
|
||||
folder: string
|
||||
}
|
||||
|
||||
export interface ComponentMeta {
|
||||
name?: string
|
||||
tags?: string[]
|
||||
props?: string[]
|
||||
imports?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a .vue file content into template, setup, style sections
|
||||
*/
|
||||
export function parseVueFile(content: string): { template: string; setup: string; style: string } {
|
||||
const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/)
|
||||
const scriptMatch = content.match(/<script\s+setup[^>]*>([\s\S]*?)<\/script>/)
|
||||
const styleMatch = content.match(/<style[^>]*>([\s\S]*?)<\/style>/)
|
||||
|
||||
return {
|
||||
template: templateMatch?.[1]?.trim() || '',
|
||||
setup: scriptMatch?.[1]?.trim() || '',
|
||||
style: styleMatch?.[1]?.trim() || ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read optional meta.json from a component folder
|
||||
*/
|
||||
export function readMetaJson(folderPath: string): ComponentMeta | null {
|
||||
const metaPath = join(folderPath, 'meta.json')
|
||||
if (!existsSync(metaPath)) return null
|
||||
|
||||
try {
|
||||
const raw = readFileSync(metaPath, 'utf-8')
|
||||
return JSON.parse(raw) as ComponentMeta
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a component folder into a VueComponentDefinition
|
||||
* Convention: user-components/counter-widget/CounterWidget.vue + optional meta.json
|
||||
*/
|
||||
export function parseComponentFolder(baseDir: string, folderName: string): VueComponentDefinition | null {
|
||||
const folderPath = join(baseDir, folderName)
|
||||
|
||||
if (!existsSync(folderPath) || !statSync(folderPath).isDirectory()) return null
|
||||
|
||||
// Find the .vue file in the folder
|
||||
const files = readdirSync(folderPath)
|
||||
const vueFile = files.find(f => f.endsWith('.vue'))
|
||||
if (!vueFile) return null
|
||||
|
||||
const vueContent = readFileSync(join(folderPath, vueFile), 'utf-8')
|
||||
const { template, setup, style } = parseVueFile(vueContent)
|
||||
|
||||
if (!template) return null
|
||||
|
||||
const meta = readMetaJson(folderPath)
|
||||
|
||||
// Derive name from meta or filename (CounterWidget.vue -> CounterWidget)
|
||||
const nameFromFile = vueFile.replace('.vue', '')
|
||||
const name = meta?.name || nameFromFile
|
||||
|
||||
return {
|
||||
id: folderName,
|
||||
name,
|
||||
template,
|
||||
setup: setup || undefined,
|
||||
style: style || undefined,
|
||||
props: meta?.props,
|
||||
imports: meta?.imports,
|
||||
tags: meta?.tags,
|
||||
folder: folderName
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all component subdirectories and parse them
|
||||
*/
|
||||
export function listAllComponents(baseDir: string): VueComponentDefinition[] {
|
||||
if (!existsSync(baseDir)) return []
|
||||
|
||||
const entries = readdirSync(baseDir)
|
||||
const components: VueComponentDefinition[] = []
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(baseDir, entry)
|
||||
if (!statSync(fullPath).isDirectory()) continue
|
||||
|
||||
const component = parseComponentFolder(baseDir, entry)
|
||||
if (component) {
|
||||
components.push(component)
|
||||
}
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
Reference in New Issue
Block a user