Spaces:
Running
Running
| 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<string>, 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<string>(); | |
| 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' }); | |
| } | |
| } | |
| }, | |
| }; | |
| } | |