import { readFileSync } from 'node:fs'; import path from 'node:path'; import type { Plugin } from 'vite'; const VIRTUAL_ID = 'virtual:shared-ui-manifest'; const RESOLVED_ID = `\0${VIRTUAL_ID}`; export interface SharedUiExport { name: string; isComponent: boolean; } const ALL_CAPS_RE = /^[A-Z][A-Z0-9_]+$/; const NON_COMPONENT_SUFFIXES = [ 'Props', 'Config', 'Options', 'Type', 'Types', 'Colors', 'Color', 'Styles', 'Style', 'Labels', 'Label', 'Status', 'Statuses', 'Mode', 'Event', 'Events', 'Context', 'Schema', 'Entry', 'Item', 'Items', 'Args', 'Params', 'Payload', 'Result', 'Response', 'Request', 'Error', 'Action', 'Actions', 'Spec', 'Def', 'Info', 'Meta', 'Map', 'Signal', 'Level', 'Severity', 'Category', ]; export function classifyName(name: string): boolean { if (!name || !/^[A-Z]/.test(name)) return false; if (ALL_CAPS_RE.test(name)) return false; for (const suffix of NON_COMPONENT_SUFFIXES) { if (name.endsWith(suffix)) return false; } return true; } function processExportItem(raw: string, seen: Set, out: SharedUiExport[]): void { const trimmed = raw.trim(); if (!trimmed) return; if (trimmed.startsWith('type ') || trimmed === 'type') return; const aliasMatch = trimmed.match(/^(\w+)\s+as\s+(\w+)$/); const finalName = aliasMatch ? aliasMatch[2] : trimmed.replace(/\s+as\s+\w+$/, '').trim(); if (!finalName || finalName === 'default' || seen.has(finalName)) return; seen.add(finalName); out.push({ name: finalName, isComponent: classifyName(finalName) }); } export function parseExportsFromIndex(indexPath: string): SharedUiExport[] { let content: string; try { content = readFileSync(indexPath, 'utf-8'); } catch { return []; } const exports: SharedUiExport[] = []; const seen = new Set(); const lines = content.split('\n'); let blockBuffer = ''; let inBlock = false; let isTypeBlock = false; for (const rawLine of lines) { const line = rawLine.trim(); if (!inBlock) { if (/^export\s+type\s+\{/.test(line)) { isTypeBlock = true; inBlock = true; if (line.includes('}')) { inBlock = false; isTypeBlock = false; } continue; } if (/^export\s+\{/.test(line)) { if (line.includes('}')) { const m = line.match(/^export\s+\{([^}]*)\}/); const inner = m?.[1]; if (inner !== undefined) { for (const item of inner.split(',')) { processExportItem(item, seen, exports); } } } else { inBlock = true; isTypeBlock = false; blockBuffer = line.replace(/^export\s+\{/, '').trim(); } continue; } const declMatch = line.match( /^export\s+(?:default\s+)?(?:function|class|const|let|var|abstract\s+class)\s+(\w+)/, ); if (declMatch && declMatch[1] !== undefined) { processExportItem(declMatch[1], seen, exports); } } else { if (line.includes('}')) { inBlock = false; if (!isTypeBlock) { const inner = `${blockBuffer},${line.replace(/\}.*/, '')}`; for (const item of inner.split(',')) { processExportItem(item, seen, exports); } } blockBuffer = ''; isTypeBlock = false; } else { if (!isTypeBlock) { blockBuffer += `,${line}`; } } } } return exports.sort((a, b) => a.name.localeCompare(b.name)); } export function sharedUiManifestPlugin(options?: { indexPath?: string }): Plugin { let resolvedIndexPath = ''; let manifestSource = ''; function buildManifest(indexPath: string): string { const allExports = parseExportsFromIndex(indexPath); const json = JSON.stringify(allExports, null, 2); return [ '// Auto-generated by sharedUiManifestPlugin — do not edit.', `export const sharedUiExports = ${json};`, ].join('\n'); } return { name: 'shared-ui-manifest', enforce: 'pre', configResolved(config) { resolvedIndexPath = options?.indexPath ?? path.resolve(config.root, '../../lib/shared-ui/src/index.ts'); manifestSource = buildManifest(resolvedIndexPath); }, resolveId(id): string | undefined { if (id === VIRTUAL_ID) return RESOLVED_ID; return undefined; }, load(id): string | undefined { if (id === RESOLVED_ID) return manifestSource; return undefined; }, handleHotUpdate({ file, server }) { if (file === resolvedIndexPath) { manifestSource = buildManifest(resolvedIndexPath); const mod = server.moduleGraph.getModuleById(RESOLVED_ID); if (mod) { server.moduleGraph.invalidateModule(mod); server.ws.send({ type: 'full-reload' }); } } }, }; }