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.
118 lines
3.1 KiB
TypeScript
118 lines
3.1 KiB
TypeScript
/**
|
|
* 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
|
|
}
|